From aae38b4d0c7902304a74919eca27338e5ea62ec9 Mon Sep 17 00:00:00 2001 From: virgoone Date: Fri, 16 Oct 2026 13:02:39 +0900 Subject: [PATCH 1/3] Document custom LLM endpoint errors on sessions and call.ended Co-Authored-By: Claude Opus 5.5 (1M context) --- agents/build/custom-llm.mdx | 21 ++++++++++++++++++- agents/monitor/conversation-history.mdx | 28 ++++++++++++++++++++++++- agents/monitor/webhooks.mdx | 7 +++++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/agents/build/custom-llm.mdx b/agents/build/custom-llm.mdx index fcf7d75..58a1d81 100644 --- a/agents/build/custom-llm.mdx +++ b/agents/build/custom-llm.mdx @@ -143,7 +143,26 @@ A custom LLM never silently falls back to a platform model, because a platform m - Each request gets one retry and a 10 second response cap. - When a generation still fails, the agent speaks a brief hold line and stays on the call. -- After three consecutive failed generations the agent apologizes, hangs up, and the session ends with `end_reason: llm_endpoint_failure` (in the [sessions API](/agents/monitor/conversation-history#status-and-end-reason), the dashboard, and the `call.ended` webhook's `ended_reason`). Each failure also emits an `llm.endpoint_error` event on the session timeline. +- After three consecutive failed generations the agent apologizes, hangs up, and the session ends with `end_reason: llm_endpoint_failure` (in the [sessions API](/agents/monitor/conversation-history#status-and-end-reason), the dashboard, and the `call.ended` webhook's `ended_reason`). + +### See what your endpoint returned + +Every failed generation is recorded, whether or not the call ended because of it. You can find them in three places: + +- **Conversations** in the dashboard shows each failure in the transcript, where it happened. +- The [sessions API](/agents/monitor/conversation-history#llm-endpoint-errors) returns them as `llm_endpoint_errors`. +- The [`call.ended` webhook](/agents/monitor/webhooks#payload) carries the same list. + +Each failure has a type: + +| `type` | Meaning | +| ------------------ | ------------------------------------------------------------------------ | +| `http_error` | Your endpoint answered with a non-2xx status | +| `timeout` | No response within 10 seconds | +| `connection_error` | The request got no answer, for example a DNS, TLS or refused connection | +| `invalid_response` | A 2xx response that was not a usable reply, such as a broken stream | + +For `http_error`, `message` holds the start of your response body, up to 300 characters. Your API key is masked. The lines the agent spoke in place of a reply are marked with `llm_failure_line: true`. Voice conversations are latency sensitive: diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index 7b2a955..1fb3780 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -189,7 +189,7 @@ curl "https://api.fish.audio/v1/agent/sessions/SESSION_ID" \ | `type` | What it is | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `message` | One utterance. `role` is `user` or `assistant` (system prompt content is never exposed). `turn_id` groups items belonging to the same conversational turn. | +| `message` | One utterance. `role` is `user` or `assistant` (system prompt content is never exposed). `turn_id` groups items belonging to the same conversational turn. `llm_failure_line` is `true` on a fixed line the agent spoke because its LLM failed. | | `tool_call` | A tool invocation as it started. `input` is a JSON string, stored in full with no truncation. | | `tool_result` | The paired outcome, matched by `call_id`. `status` is `completed` or `failed`. `latency_ms` is the execution time. | | `transfer` | One [call transfer](/agents/telephony/transfers) attempt, successful or not. See [Transfer items](#transfer-items). | @@ -203,6 +203,32 @@ Details worth knowing: - **Mid-call queries**: you can fetch an `active` session; you get the items persisted so far, and `analysis` is `null` until [post-call analysis](/agents/monitor/post-call-analysis) completes. - A `tool_call` without a matching `tool_result` means the execution never resolved. +### LLM endpoint errors + +Sessions on a [custom LLM](/agents/build/custom-llm) also carry `llm_endpoint_errors`. It lists each failed generation of your endpoint, oldest first. It is empty when the agent runs on a platform model or your endpoint never failed. + +```json +{ + "end_reason": "llm_endpoint_failure", + "llm_endpoint_errors": [ + { + "occurred_at": "2026-10-15T15:45:10Z", + "type": "http_error", + "status_code": 502, + "message": "no tunnel", + "duration_ms": 812, + "consecutive_failures": 1 + } + ] +} +``` + +- `type` is `http_error`, `timeout`, `connection_error` or `invalid_response`. See [Failure behavior](/agents/build/custom-llm#failure-behavior). New types may be added. +- `status_code` is set on `http_error` only. +- `message` is the start of your response body, or the cause of the failure. It is at most 300 characters, and your API key is masked. +- `duration_ms` is the time from the request to the response or the failure. It is `null` when it was not measured. +- `consecutive_failures` counts failures in a row. The agent hangs up at the third. + `items` is a discriminated union on `type`, and it evolves additively. Ignore item types and fields you don't recognize. New modalities and item kinds will diff --git a/agents/monitor/webhooks.mdx b/agents/monitor/webhooks.mdx index 0b2a7cb..6943aa3 100644 --- a/agents/monitor/webhooks.mdx +++ b/agents/monitor/webhooks.mdx @@ -92,7 +92,7 @@ The list is replaced as a whole on each update, so send every endpoint you want ## Payload -Every payload carries the `event` name and a `session` object with the session facts. Note the field names differ from the [sessions API](/agents/monitor/conversation-history), which uses `session_id` / `started_at` / `ended_at` for the same facts. `call.ended` adds `ended_reason`; `call.analyzed` adds the `analysis` entity; `phone_call.dial_finished` adds top-level `dial_status` and `answered_by`. The `session` object is the same on every event. The `phone_call.dial_finished` example shows it in full, the others are shortened. +Every payload carries the `event` name and a `session` object with the session facts. Note the field names differ from the [sessions API](/agents/monitor/conversation-history), which uses `session_id` / `started_at` / `ended_at` for the same facts. `call.ended` adds `ended_reason` and `llm_endpoint_errors`; `call.analyzed` adds the `analysis` entity; `phone_call.dial_finished` adds top-level `dial_status` and `answered_by`. The `session` object is the same on every event. The `phone_call.dial_finished` example shows it in full, the others are shortened. @@ -114,7 +114,8 @@ Every payload carries the `event` name and a `session` object with the session f "agent_name": "Support agent", "config_hash": "sha256:9c41…" }, - "ended_reason": "user_hangup" + "ended_reason": "user_hangup", + "llm_endpoint_errors": [] } ``` @@ -187,6 +188,8 @@ Every payload carries the `event` name and a `session` object with the session f `ended_reason` is the same value as `session.end_reason` and uses the [sessions API vocabulary](/agents/monitor/conversation-history#status-and-end-reason): `user_hangup`, `agent_hangup`, `escalated`, `conversation_timeout`, `heartbeat_timeout`, `agent_join_timeout`, `internal_error`, or `llm_endpoint_failure`. `call.ended` is only sent for sessions in which a participant connected, so the never-connected reasons (`room_join_timeout`, `cancelled`, `dial_failed`) do not appear here; query the sessions API with `status=failed` for those. In the rare case the reason is not settled yet at delivery time, both fields are `null`. New values may be added; treat unrecognized values as informational rather than rejecting the event. +`call.ended` also carries `llm_endpoint_errors`. It lists the failures of your [custom LLM](/agents/build/custom-llm#see-what-your-endpoint-returned) endpoint during the session, in the same shape as the [sessions API](/agents/monitor/conversation-history#llm-endpoint-errors). It is empty on platform models. It is `null` in the rare case the list could not be read at delivery time. + `dial_status` is `answered`, `busy`, `no_answer`, or `failed`. `answered_by` is deprecated: answering-machine detection has been removed, so it is `unknown` once the call is answered and `null` before (older events may carry `human` or `voicemail`). Both facts also appear on the session, alongside `direction` (`inbound` or `outbound`) and `batch_call_id` (reserved, always `null` today); these session fields are present in every webhook payload, with the dial fields `null` on inbound sessions. The `phone_call.dial_finished` snapshot is taken when the dial resolves, so on an answered call `conversation_ended_at` and `duration_seconds` are still `null`. The final numbers arrive with `call.ended`. `caller_number`, `dialed_number` and `sip_call_id` carry the same values as on the [sessions API](/agents/monitor/conversation-history): the E.164 numbers of the call (person-centric on outbound calls, so `caller_number` is the party you dialed) and the SIP `Call-ID` of the call's INVITE. All three are empty strings on web sessions, and `sip_call_id` is also empty on an unanswered outbound dial. How to use it against your carrier's records: [Match calls with your SIP records](/agents/telephony/byo-sip#match-calls-with-your-sip-records). From 3ada28779c6df43a17e413c4e5e498578732f2da Mon Sep 17 00:00:00 2001 From: virgoone Date: Fri, 16 Oct 2026 16:38:47 +0900 Subject: [PATCH 2/3] Rename the fallback-line marker to fallback_reason Co-Authored-By: Claude Opus 5.5 (1M context) --- agents/build/custom-llm.mdx | 2 +- agents/monitor/conversation-history.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/build/custom-llm.mdx b/agents/build/custom-llm.mdx index 58a1d81..7d1349e 100644 --- a/agents/build/custom-llm.mdx +++ b/agents/build/custom-llm.mdx @@ -162,7 +162,7 @@ Each failure has a type: | `connection_error` | The request got no answer, for example a DNS, TLS or refused connection | | `invalid_response` | A 2xx response that was not a usable reply, such as a broken stream | -For `http_error`, `message` holds the start of your response body, up to 300 characters. Your API key is masked. The lines the agent spoke in place of a reply are marked with `llm_failure_line: true`. +For `http_error`, `message` holds the start of your response body, up to 300 characters. Your API key is masked. The lines the agent spoke in place of a reply have `fallback_reason: "llm_error"`. Voice conversations are latency sensitive: diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index 1fb3780..edb0fd8 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -189,7 +189,7 @@ curl "https://api.fish.audio/v1/agent/sessions/SESSION_ID" \ | `type` | What it is | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `message` | One utterance. `role` is `user` or `assistant` (system prompt content is never exposed). `turn_id` groups items belonging to the same conversational turn. `llm_failure_line` is `true` on a fixed line the agent spoke because its LLM failed. | +| `message` | One utterance. `role` is `user` or `assistant` (system prompt content is never exposed). `turn_id` groups items belonging to the same conversational turn. `fallback_reason` is `"llm_error"` on a fixed line the agent spoke because its LLM failed, and `null` otherwise. | | `tool_call` | A tool invocation as it started. `input` is a JSON string, stored in full with no truncation. | | `tool_result` | The paired outcome, matched by `call_id`. `status` is `completed` or `failed`. `latency_ms` is the execution time. | | `transfer` | One [call transfer](/agents/telephony/transfers) attempt, successful or not. See [Transfer items](#transfer-items). | From cd0b6c1e47f6112d2ac2cc28f99e32191835d0c0 Mon Sep 17 00:00:00 2001 From: virgoone Date: Mon, 19 Oct 2026 14:24:51 +0900 Subject: [PATCH 3/3] Describe LLM endpoint errors with turn_id, reason and response_body Co-Authored-By: Claude Opus 5.5 (1M context) --- agents/build/custom-llm.mdx | 2 +- agents/monitor/conversation-history.mdx | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/agents/build/custom-llm.mdx b/agents/build/custom-llm.mdx index 7d1349e..b197491 100644 --- a/agents/build/custom-llm.mdx +++ b/agents/build/custom-llm.mdx @@ -162,7 +162,7 @@ Each failure has a type: | `connection_error` | The request got no answer, for example a DNS, TLS or refused connection | | `invalid_response` | A 2xx response that was not a usable reply, such as a broken stream | -For `http_error`, `message` holds the start of your response body, up to 300 characters. Your API key is masked. The lines the agent spoke in place of a reply have `fallback_reason: "llm_error"`. +For `http_error`, `response_body` holds the start of your response body, up to 300 characters. Your API key is masked. `connection_error` and `invalid_response` also carry a `reason`, such as `dns_failed` or `not_event_stream`. The lines the agent spoke in place of a reply have `fallback_reason: "llm_error"`. Voice conversations are latency sensitive: diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index edb0fd8..cb2a9d2 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -213,9 +213,11 @@ Sessions on a [custom LLM](/agents/build/custom-llm) also carry `llm_endpoint_er "llm_endpoint_errors": [ { "occurred_at": "2026-10-15T15:45:10Z", + "turn_id": 3, "type": "http_error", + "reason": null, "status_code": 502, - "message": "no tunnel", + "response_body": "{\"detail\":\"no tunnel\"}", "duration_ms": 812, "consecutive_failures": 1 } @@ -223,9 +225,13 @@ Sessions on a [custom LLM](/agents/build/custom-llm) also carry `llm_endpoint_er } ``` +- `turn_id` is the turn the failed reply belonged to. It uses the same numbering as `message` items. - `type` is `http_error`, `timeout`, `connection_error` or `invalid_response`. See [Failure behavior](/agents/build/custom-llm#failure-behavior). New types may be added. +- `reason` is set on `connection_error` and `invalid_response` only. New reasons may be added. + - `connection_error`: `dns_failed`, `connection_refused`, `connection_reset`, `tls_failed`, `address_not_allowed` (an address the platform does not call) or `unknown`. + - `invalid_response`: `not_event_stream` (a streamed request got something else back), `stream_incomplete` (the stream ended with no `finish_reason`, usage or output), `empty_reply` (no text and no tool call) or `unknown`. - `status_code` is set on `http_error` only. -- `message` is the start of your response body, or the cause of the failure. It is at most 300 characters, and your API key is masked. +- `response_body` is the start of what your endpoint sent back, on `http_error` and `not_event_stream` only. It is at most 300 characters, and your API key is masked. It is `null` when the body was empty. - `duration_ms` is the time from the request to the response or the failure. It is `null` when it was not measured. - `consecutive_failures` counts failures in a row. The agent hangs up at the third.