A custom LLM agent is tested differently from a dashboard agent. The Test LLM playground, the simulated user and simulation test cases run the built-in prompt engine, not your server, so the custom LLM builder does not show the test panel or the Simulation tab. Use the tools on this page instead.
| Tool | What it checks | Needs keys |
|---|---|---|
| Test connection in the builder | The URL is reachable and your server sends a frame | No |
| Voice Orb link, web call or phone call | The full call path with speech, turn-taking and actions | Yes (LiveKit, plus telephony for phone calls) |
Text simulator (python -m tried_agent.simulate) |
The protocol and your replies, turn by turn, without audio | No |
Test connection#
The LLM WebSocket card in the custom LLM builder has a Server URL field and a Test connection button.
Enter the URL#
Type the base URL, for example wss://llm.example.com/llm-websocket. The field turns red and shows "Use a ws:// or wss:// URL." for anything else. The value is saved to the agent when the field loses focus.
Choose Test connection#
The URL is saved first, then the API runs the test.
Read the result#
Connected in N ms shows the time to your server's first frame and the frame itself (up to 2,000 characters). Couldn't connect shows the reason.
What the test does#
The API (POST /test-custom-llm-connection) does this:
- Validates the URL and rejects anything that is not
ws://orwss://. - Resolves the host and refuses private, loopback and reserved addresses, the same rule as other outbound requests. Set
EGRESS_ALLOW_PRIVATE=1on the API to allow them in local development. - Opens
{url}/test_connection_<24 hex chars>. - Sends a
call_detailsframe withcall_type: "web_call",agent_id: "test_connection"andcall_status: "registered", then aping_pongframe. - Waits up to 5 seconds for any frame from your server.
Your server passes if it sends any frame within 5 seconds, for example the config frame it sends on open.
The test sends call_details without waiting for your config frame, unlike a real call. A server that only accepts call_details after it has sent config should ignore the order during the test.
Test connection errors#
| Message | Cause |
|---|---|
| Not a valid URL | The value could not be parsed as a URL. |
| Use a ws:// or wss:// URL | Wrong scheme, for example https://. |
| A host or egress error | The host resolves to a private or reserved address, or cannot be resolved. |
| Could not connect to the WebSocket server | TCP, TLS or the WebSocket upgrade failed. |
| The server closed the connection (code N) before sending a frame | Your server accepted and then closed the socket. |
| No frame within 5 s | Your server accepted but sent nothing. Send config immediately on open. |
The endpoint needs the admin or developer role:
curl -X POST https://api.tried.com.au/test-custom-llm-connection \
-H "Authorization: Bearer $TRIED_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "llm_websocket_url": "wss://llm.example.com/llm-websocket" }'{
"ok": true,
"latency_ms": 184,
"first_frame": "{\"response_type\":\"config\",\"config\":{\"auto_reconnect\":true,\"call_details\":true}}",
"error": null
}Live calls#
Needs keys Live audio needs LiveKit configured on the API and a running voice worker. Phone calls also need a telephony provider.
Three ways to talk to a custom LLM agent:
- Voice Orb link. Use the share button in the agent header to open Share with a Voice Orb link, choose Create link, and open it in a browser. Calls are billed to the workspace. See Testing with audio.
- Web call through the API. Call create web call with the agent id and join the returned room with the web SDK.
- Phone call. Attach the agent to a number and call it, or use Make an outbound call on Phone Numbers. See Phone testing.
During a live call, check:
- The opening line plays if you send
response_id: 0. - The agent stops talking when you interrupt, and your server receives a new
response_requiredwith a higher id. - A long silence produces
reminder_required. end_call,transfer_numberanddigit_to_presswork (transfers and digits need a phone call).
Afterwards, open the call in Call History. The transcript shows your tool call events, and the disconnection reason tells you whether the socket failed. The WebSocket round trip is stored on the call as latency.llm_websocket_network_rtt (available from the API).
Text simulator#
The voice worker ships a text simulator that runs the same engine as a live call, without audio, LiveKit or keys. With a custom LLM snapshot it connects to your server and drives it with typed lines.
Create snapshot.json:
{
"call": { "call_id": "call_local_test_0001", "call_type": "phone_call", "direction": "inbound",
"from_number": "+61411111111", "to_number": "+61355505030" },
"agent": { "agent_id": "agent_local" },
"engine": { "type": "custom-llm", "llm_websocket_url": "ws://127.0.0.1:8080/llm-websocket" },
"voice": { "provider": "elevenlabs", "provider_voice_id": "x", "model": "eleven_flash_v2_5" },
"knowledge_base_ids": [], "dynamic_variables": {}, "api_base": "http://api.local"
}Write the caller's lines in lines.txt, one per line. Special lines:
| Line | Meaning |
|---|---|
<silence> |
The caller stays silent. After the caller has spoken once, this triggers a reminder, so your server gets reminder_required |
<dtmf:123#> |
The caller presses keypad digits |
<hangup> |
The caller hangs up |
# ... |
Comment |
Run it from services/agent:
.venv/bin/python -m tried_agent.simulate --snapshot snapshot.json --script lines.txt --events -Without --script it reads lines interactively. --events - prints every runtime event (utterances, tool calls, latency, call_ended with the disconnection reason) as JSON. Simulator runs stop at 10 minutes or 400 utterances.
The simulator waits about one second for your server to send the opening line (response_id: 0) before the first caller line. Connection failures end the run with the same error_llm_websocket_* reasons as a live call.
Testing checklist#
-
configis the first frame and arrives within 0.5 seconds. - Every
ping_pongis echoed with the sametimestamp. - Responses for an old
response_idstop when a newer request arrives. - The last frame of every response has
content_complete: true. - No frame gap within one response reaches 30 seconds.
-
reminder_requiredgets a short reply. - Unknown
interaction_typevalues are ignored, not treated as errors. - The server survives a reconnect: it can receive
call_detailsand the in-flightresponse_requiredagain.