Added
-
Added
image_urlsupport for Gemini adapter, enabling external URLs viaPart.from_uri().
(PR #3573) -
Added Google Speech-to-Text v2
adaptationsupport toGoogleSTTService, so recognition can be biased toward domain terms using inline or referenced phrase sets. Configurable at construction and updatable at runtime.
(PR #4413) -
Added
MCPClient(tools_arguments=...), which injects extra arguments into every call of a tool. Use it for arguments the model shouldn't choose — a fixed search mode, an account id, a caller-supplied filter. The pinned arguments override anything the model supplies, and are hidden from the schema it sees:mcp = MCPClient( server_params=..., tools_arguments={"search": {"mode": "realtime"}}, )
In this example, the model only ever sees
search(query=...), while every call reaches the server assearch(query=..., mode="realtime").
(PR #4939) -
Added
MCPClient.tools():LLMContext(tools=await mcp.tools())is now all you need to use MCP tools — connecting, tool registration, and closing the connection at pipeline end are automatic.
(PR #4939) -
Added
KeenableWebSearch(pipecat.services.keenable.search), an optional service that gives voice agents live web search and page reading via a hosted MCP server powered by Keenable AI. It exposes the server'ssearch_web_pages(with optional site and date-range filters) andfetch_page_contenttools — passawait search.tools()to yourLLMContextand the tools register automatically (the connection is released automatically when the pipeline ends, too). Install with thekeenableextra. Works keyless (promode); passapi_key=for higher rate limits and access to the lower-latencyrealtimemode (requires an account with realtime mode enabled), selected withmode="realtime".
(PR #4942) -
Added the Pipecat Context Hub to the
cliextra, souv tool install "pipecat-ai[cli]"providespipecat context-hub(aliaspipecat ch) with no separate install — the guidespipecat initwrites tell coding agents to query the hub, so the CLI ships it. Costs about 195 MB on top of the extra; bot runtimes are unaffected, sinceclistays optional so base images remain lean. The scaffolded agent guides teachpipecat context-hubas the primary way to query the hub, withuvx pipecat-ai-context-hubas the no-install fallback.
(PR #5122) -
Added Context Hub setup to
pipecat init. On the coding-agent path it registers the hub's MCP server with each coding agent CLI it finds, and says what came of it: Cursor, VS Code, and Zed are configured by hand, so it points atpipecat context-hub installto print the config block to paste, and a client that rejects the registration reports why. It then offers to build the local index when there isn't one — a few minutes and roughly 900 MB, so it asks rather than assumes. The question only appears while no index exists, so it doesn't return once you have one.pipecat init quickstartskips setup entirely to stay a short path to a running bot, and--no-context-hubopts out anywhere.
(PR #5122) -
Added a Pipecat Context Hub freshness notice to the CLI. When a local hub index exists and has gone stale, or was built for a different
pipecat-aiminor than the project in the working directory, the CLI prints a one-line hint on stderr suggestingpipecat context-hub refresh— so a coding agent citing an API that has since changed is caught before the generated code is. The check reads the hub's published index metadata directly with the standard library, adding no dependency and no meaningful startup cost. It is silent when no index exists, compares onlymajor.minor(ignoring patch and dev segments), stays quiet for editable pipecat checkouts, and can be switched off withPIPECAT_HUB_CHECK=0; the staleness threshold shares the hub's ownPIPECAT_HUB_STALE_AFTER_DAYS.
(PR #5122) -
Added
ProposedUserStartedSpeakingFrameandProposedUserStoppedSpeakingFrame, the way a service with its own turn detection tells the pipeline where it thinks a turn boundary falls.ExternalUserTurnStrategiesresolve those proposals intoUserStartedSpeakingFrame/UserStoppedSpeakingFrame, so the strategies are a single place that decides turns and can be subclassed to adjust the timing — previously a service that emitted turn frames took the reins entirely and left nothing to extend. Seeexamples/turn-management/turn-management-custom-external-turn-strategy.pyfor a stop strategy that holds the turn open past the service's proposal so a trailing afterthought can reopen it.Every in-repo service with built-in turn detection now emits proposed turn frames rather than real turn frames: the AssemblyAI, Cartesia Ink-2, Deepgram Flux, Gladia, Sarvam, Soniox, Speechmatics, and OpenAI Realtime STT services, and the OpenAI, xAI, and Inworld realtime LLM services. Third-party services that emit
UserStartedSpeakingFrame/UserStoppedSpeakingFramedirectly keep working unchanged; switching them to the proposal frames hands interruption handling back to the pipeline.OpenAIRealtimeSTTService(the transcription-only service, not the speech-to-speechOpenAIRealtimeLLMService) andSarvamSTTServicenow also recommendExternalUserTurnStrategieswhen their server-side VAD is enabled, matching the other turn-detecting services. Their turn frames were previously informational and nothing in the pipeline acted on them.
(PR #5156) -
Enabled MoQ client mode, where the bot and the browser both dial a relay and rendezvous there instead of the bot serving its own socket. Since neither side needs a reachable address, this works when the bot is behind NAT.
- Select it by naming a relay:
python bot.py -t moq --moq-connect https://cdn.moq.dev/anon. Without--moq-connectthe bot serves its own socket, as before. - Each client-mode session gets its own random namespace, so concurrent sessions on a shared relay don't collide. Pass
--moq-namespaceto pin a well-known room instead. - Added
MOQParams.response_pathandMOQParams.request_path, which set the bot's broadcast paths directly (the bot publishes itsresponse_path, subscribes to the peer'srequest_path) instead of deriving them fromnamespace+participant_id/peer_id. - The namespace layer needs both peers to agree on a namespace up front. These are for deployments where the paths are assigned externally instead — e.g. a host that runs one bot per caller and names both paths after an id the caller minted, so there's no namespace to agree on.
- Either can be set alone; the other still derives from the namespace. Unset, behaviour is unchanged.
- The default participant ids are now named by direction: the bot publishes under
<namespace>/responseand subscribes to the peer at<namespace>/request(previouslybot0/client0).--moq-bot-id/--moq-client-idstill override them.
(PR #5158)
- Select it by naming a relay:
-
Added an optional
languagekey to the eval harness's built-inuser.speech:andjudge.transcription:blocks. Each built-in speech service builder (kokoro,cartesia,whisper,moonshine) now forwardslanguage(a code likezhor aLanguage) into the service settings, so non-English audio evals can synthesize user turns and transcribe bot audio in the right language without thefactory:escape hatch. Omittinglanguageis unchanged; the TTS audio cache key now includes the language so English and non-English renders of the same text don't collide.
(PR #5171) -
Added
JobParamsandJobGroupParams, which carry everything a job dispatch needs in one object:name,payload,timeout, pluscancel_on_errorfor groups andlabel/cancellablefor how the work presents to a client UI. Pass one tojob(...),job_group(...),request_job(...), orrequest_job_group(...):job_id = await ui_jobs.request_job_group( "wikipedia", "news", params=JobGroupParams(payload={"query": query}, label=f"Research: {query}"), )
(PR #5221)
-
Added
BaseUIWorker, a worker that surfaces its jobs and job groups on the client UI without involving an LLM. Every group it dispatches streams its lifecycle to the client as the standardui-job-groupenvelopes, with the client's reserved__cancel_job_groupevent honored for groups dispatched as cancellable. Dispatch from a plainBaseWorkerwhen the work should stay invisible. It is instantiable directly, so an app can register one on the runner as a dispatcher and call it from a tool, andUIWorkernow inherits from it, keeping the same capability for a page-driving LLM worker.BaseWorkeritself is unchanged. Theasync-tasksexample fans out research through aBaseUIWorkerdispatcher driven by the main pipeline's own LLM tool, one LLM instead of two, whiledocument-reviewkeeps itsUIWorker, which reads and drives the page content its review depends on.
(PR #5221) -
Added
WorkerRunner.get_worker(name), which returns a worker added to that runner, along withBaseWorker.worker_runner,FrameProcessor.worker_runner, andFunctionCallParams.worker_runnerto reach the runner from inside a worker, a processor, or a tool handler. A tool that needs a peer worker can now find it by name rather than having the application pass the object in throughapp_resources:async def research(params: FunctionCallParams, query: str): ui_jobs = params.worker_runner.get_worker("ui-jobs")
Only workers on the same runner have a local instance to return; a worker on another runner is addressable over the bus but has no object to hand back.
(PR #5221) -
Added
BaseWorker.request_cancel_job_group(job_id, reason=...), the door for cancellation asked for from outside the worker. It honors the request only for a group dispatched withJobGroupParams(cancellable=True)and returns whether it did, so a client UI, an operator endpoint, or anything else reaching in gets the same rule. Cancellation the worker decides on itself, on shutdown, on a timeout, or throughcancel_on_error, still callscancel_job_group()and is never refused.
(PR #5221) -
Added an
extra_headersargument toCartesiaSTTService,CartesiaTTSServiceandCartesiaHttpTTSService, matchingCartesiaTurnsSTTService. The headers are sent with the websocket handshake (or with each synthesis request, forCartesiaHttpTTSService), so deployments can supply their own authentication or routing headers.
(PR #5223) -
Added
AudioVolumeTracker(pipecat.audio.volume), which measures the volume of an audio stream over a rolling 400ms window. Audio is fed in chunks of any size withupdate(audio, sample_rate)and read back from thevolumeproperty, which reads 0 until the window holds enough audio to be measurable. Measuring happens on read and is cached until more audio arrives, so callers that report volume less often than they receive audio pay only for the reads.VADAnalyzerandRTVIObserverboth track volume through it.
(PR #5232) -
AICFilterandAICQuailVADAnalyzernow close their ai-coustics session when the pipeline stops, instead of waiting for garbage collection.
(PR #5239) -
Added
PipelineWorker(processor_unusable_policy=...), deciding what the pipeline does when a processor reports an error that leaves it unable to do its job (becomesis_usable=False), such as a service whose API key was rejected.ProcessorUnusablePolicy.CONTINUE(the default) keeps the pipeline running and leaves the decision to the application, whileENDandCANCELstop it gracefully or immediately. It is applied once per processor, not once per failed request:```python worker = PipelineWorker( pipeline, processor_unusable_policy=ProcessorUnusablePolicy.END, ) ```(PR #5242)
-
Added
FrameProcessor.is_usable, reporting whether a processor can still do its job, so applications can tell one that's briefly struggling from one that will never work again until something changes.A processor stays usable through failures it might recover from, and becomes unusable once its work can no longer succeed: a provider has rejected its API key, model or voice, or it has failed enough times to stop trying. Services stop accepting work and stop reconnecting once that happens, instead of retrying something that will keep failing.
Errors set it as they are reported, so an error handler reading
frame.processor.is_usablealways sees the verdict that came with the error it is handling. This works in a worker'son_pipeline_errorhandler, which sees every error in the pipeline:```python @worker.event_handler("on_pipeline_error") async def on_pipeline_error(worker, frame): if frame.processor and not frame.processor.is_usable: logger.error(f"{frame.processor} can no longer do its job:{frame.error}")
```and equally in a single processor's own
on_errorhandler, when only one service is of interest:```python @tts.event_handler("on_error") async def on_error(processor, frame): if not processor.is_usable: logger.error(f"TTS can no longer do its job: {frame.error}") ```Changes are also reported through
on_usable_changed, a new event handler on the processor, which fires on the transition rather than on every error.Bring a processor back with
set_usable(True)once whatever stopped it working has been dealt with. Services do this for themselves whenever their settings change, since a new model or voice may be exactly the fix. Credentials aren't runtime settings, so a rejected API key needs either a new service or an explicitset_usable(True).
(PR #5242) -
Added
ErrorCategory, recording what kind of failure an error was — a rejected API key (AUTHENTICATION) versus a provider outage (SERVER), for example.ErrorFramecarries it in a newcategoryfield, andFrameProcessor.push_error()accepts it as an argument:```python await self.push_error("rejected API key",category=ErrorCategory.AUTHENTICATION)
```The category says what went wrong, not what became of the processor. To decide whether a processor is worth using again, read
processor.is_usable.Every error reaching a handler carries a category;
ErrorCategory.UNKNOWNmeans the cause couldn't be determined, which handlers can treat the way they treated every error before.FrameProcessor.push_error()andpush_error_frame()also take aforce_treat_as_permanentargument, for an error that will keep recurring and so leaves the processor unable to do any more work. It's only needed for failures the category doesn't already convey, such as a websocket service exhausting its reconnection attempts; leaving it unset doesn't keep the processor usable, since a permanent category costs it itsis_usableon its own.A category is worked out from the exception only when the reporter left it unset, so an error is never mistaken for a verdict on a processor it didn't come from:
- Failures in application code a service invoked are reported as
ErrorCategory.APPLICATION. A tool handler or TTS text transformer whose own API call returns 401 leaves the service usable, since its credentials were never in question. - Errors caught by a broad
except, which may not have come from the processor at all, are reported asErrorCategory.UNKNOWN.
Classification falls back to the HTTP status code the exception carries. Processors whose provider signals failures through SDK-specific exceptions, or whose credentials can be rejected for reasons a reconnection would clear, refine it by overriding
_classify_error():```python class MyService(TTSService): def _classify_error(self, exception: Exception) -> ErrorCategory |None:
if isinstance(exception, MyProviderAuthError):
return ErrorCategory.AUTHENTICATION
return None
```
(PR #5242) - Failures in application code a service invoked are reported as
-
A scenario's
judge:block accepts anextra:mapping, forwarded to the judge model as top-level request parameters. This is how provider-specific options reach the judge; the default judge usesreasoning_effort: noneso that a thinking-capable model does not spend latency, or the token budget its verdict needs, on reasoning that is never read.
(PR #5243) -
GoogleLLMServicenow logs a warning naming Gemini'sfinish_reasonwhen a response ends for a notable reason — withheld for safety or recitation, a rejected tool call, or truncated at the output token limit. Previously these ended the turn with little or no text and no indication why. Whatever text did arrive is still passed downstream, and responses ending normally are unaffected.
(PR #5248) -
GoogleLLMServicenow bounds how long it waits for a streamed response, via a newstream_idle_timeout_secsargument that defaults to 20 seconds. Previously a stream that stopped producing without closing left the turn open indefinitely, since the API client applies no timeout of its own. Reaching the timeout fireson_completion_timeout, pushes anErrorFrame, and closes the response, so the pipeline continues with whatever text arrived. The timeout covers the gap between chunks rather than the response as a whole, leaving a slow but healthy stream free to take as long as it needs. Raise it for models configured to think at length, since thinking emits no chunks, or passNoneto wait indefinitely.
(PR #5249) -
Added
FrameProcessor.pause_processing_all_frames_until(ready, timeout=...), which holds frames arriving at a processor until a condition resolves and then delivers them in order. Useful for a processor that establishes a connection in the background and cannot act on frames the moment it starts.readyis anything awaitable, typically anasyncio.Event.waitthe processor already owns, so each service decides what "ready" means. The pause takes hold from the frame after the one being processed, so aStartFramethat triggers it still travels on downstream and pipeline startup is not delayed. Both frame queues are held, sotimeoutbounds the wait and the pause is always lifted, at the latest during cleanup.
(PR #5254) -
Added full client/server coverage for xAI Voice Agent item truncate/delete,
force_message, idle-timeout / DTMF / MCP event hooks, and session fields (reasoning,resumption,replace, transcription, VAD idle timeout) onGrokRealtimeLLMService.
(PR #5255) -
Added Speechify to the text-to-speech services offered by
pipecat create, which scaffolds a bot wired toSpeechifyHttpTTSServiceand addsSPEECHIFY_API_KEYandSPEECHIFY_VOICE_IDto the generated project'senv.example.
(PR #5259) -
Added
SpeechifyHttpTTSService, a Speechify text-to-speech service backed by the/v1/audio/stream/with-timestampsendpoint. Audio and word-level speech marks arrive together over Server-Sent Events, so bot speech is attributed to the conversation context word by word and an interruption commits only the portion actually spoken. Speech marks require a streaming-native model: the service defaults tosimba-3.2(English), andsimba-3.0covers the other supported languages.```python from pipecat.services.speechify.tts import SpeechifyHttpTTSService tts = SpeechifyHttpTTSService( api_key=os.environ["SPEECHIFY_API_KEY"], aiohttp_session=session, settings=SpeechifyHttpTTSService.Settings(voice="geffen_32"), ) ```(PR #5259)
-
Every eval suite run writes a
results.jsonlnext to its logs, one line per run with its outcome, its failures, and paths to its artifacts, appended as each run finishes so an interrupted sweep keeps everything already done. Runs that didn't pass also carryevents_seen, the record of what the bot actually did. Each failure carries a machine-readablekind(timeout,judge_no,missing_function_call, ...; seeFAILURE_KINDSinpipecat.evals.harness), which groups failures across many runs in a way the judge's free-text reasons cannot.
(PR #5260) -
An eval suite can run each (bot, scenario) pair several times, via
repeat:in the manifest or--repeat Nonpipecat eval suite, and reports a pass rate per pair instead of a single verdict. This is how a behavior with a race in it — interruptions, async function results, turn detection — gets measured rather than sampled, since a bot that passes half the time looks identical to a reliable one in a single pass. Attempts interleave across bots (A#1, B#1, C#1, A#2, ...) so every bot meets the same machine conditions in the same stretch of the sweep, and each attempt's number joins its artifact filenames so nothing is overwritten. A repeated sweep always exits 0: it reports a rate, and what rate is acceptable is the caller's policy.
(PR #5260) -
Added
retry_on_timeoutandretry_timeout_secstoGoogleLLMService, matching the OpenAI, Anthropic, and AWS services. Withretry_on_timeoutset, a request whose first chunk doesn't arrive withinretry_timeout_secsis issued once more, so a request the API accepts and then never answers costs a few seconds instead of the whole idle timeout. Only the first chunk is retried, since re-issuing after that would duplicate the response. Gemini's client sends the request lazily, when the first chunk is pulled, so the window spans the whole round trip including any thinking the model does before it emits anything — leave it off for models that think at length.
(PR #5262) -
Added
gemma4,glm5.2, andsarvam-105b-conversationsmodel support toSarvamLLMService.gemma4adds vision (inline data-URI image input),glm5.2adds reasoning support, andsarvam-105b-conversationstargets multi-turn conversation on the/v1endpoint. The base URL is resolved automatically from the model (/v1forsarvam-105b-conversations,/v2for all others), and switching models at runtime recreates the client when the API version changes. Model-specific capabilities — vision,reasoning_effort, andwiki_grounding— are gated to the models that support them.
(PR #5288) -
Added Icelandic, Sundanese, and Uzbek to the languages
SonioxTTSServicecan speak.
(PR #5295) -
DeepgramFluxTTSServicenow supports Flux'sspeedandexpressivityvoice controls, set viaDeepgramFluxTTSService.Settingsand updatable at runtime with aTTSUpdateSettingsFrame. A speed change is applied to the open connection with Flux'sConfiguremessage, so the cross-turn acoustic state survives it; expressivity is fixed when the connection opens, so a change reconnects. A settings update Deepgram rejects is reported as a non-fatalErrorFrame.
(PR #5296) -
Added LiveKit as a transport option in the development runner:
python bot.py -t livekit, andPOST /startsupport ("transport": "livekit"). RequiresLIVEKIT_URL,LIVEKIT_API_KEY, andLIVEKIT_API_SECRETto be configured on the server.
(PR #5297) -
Added
SarvamRealtimeSTTServicefor low-latency streaming speech-to-text with Sarvam'ssaaras:v3-realtimemodel. Supports server-side endpointing (endpointing="vad") and pipeline-driven endpointing (endpointing="manual"), interim and final transcripts, timestamps, and in-band configuration updates viaconfig.update.
(PR #5301) -
Added
cancellable_by_llmto@tool_optionsandregister_function(), which lets the LLM stop a running async tool call whose result the user no longer wants.A tool that opts in is advertised alongside its own
cancel_<name>, which stops the one call of it that's running, and takes atool_call_idonly when several calls of that tool are running at once. A tool that doesn't opt in has no cancel tool and can't be stopped.Only applies when the
cancel_on_interruption=False@tool_optionsis set. Consider using for long-running tool calls that a user might want to cancel, such as a long report or a background job that keeps producing results. The work has to outlast the LLM's route to cancelling it.```python @tool_options(cancel_on_interruption=False, cancellable_by_llm=True) async def write_report(params: FunctionCallParams, topic: str): """Write a long research report on a topic. Args: topic: What the report should cover. """ ... ```(PR #5304)
-
Added OpenClaw Gateway support in
pipecat.services.openclaw, for driving an OpenClaw coding agent from a pipeline.OpenClawGatewayServicestarts a run on anOpenClawSendFrame, redirects the one in flight on anOpenClawSteerFrame, and stops it on anOpenClawAbortFrame. A run answers with anOpenClawStartedFrame, any number ofOpenClawTextFrames, and oneOpenClawEndFramesaying whether it completed, was cancelled, or failed.OpenClawGatewayClientspeaks the same protocol without a pipeline.examples/multi-worker/openclaw-agentis a voice front end built on it.
(PR #5308) -
Added the
function_call_stoppedscenario event topipecat.evals, which reports a function call ending with thetool_call_idand acancelledflag in itsargs. It takes the samecalls:shape asfunction_call, so a scenario can assert how a call ended — telling work that was stopped from work that finished on its own, which a check on what the bot said about it cannot.```yaml - event: function_call_stopped calls: - name: write_report args: { cancelled: true } ```(PR #5314)
-
Added
setup_timeout_secsandstart_timeout_secstoPipelineWorker, both defaulting to 20 seconds. A processor that blocks while connecting, or while handling theStartFrame, would leaverun()waiting on it forever; the pipeline is now torn down once the timeout elapses.
(PR #5316) -
Added
acquiresandreleases(pipecat.utils.shared) for a resource shared by several processors, such as the client an input and an output transport share. The first owner to acquire runs the decorated method while the rest wait for it, and only the last owner to release runs the undo. A method that raises is not attempted again: the exception reaches every owner, so the two halves of a transport either both come up or both fail.```python class MyTransportClient: @acquires("client") async def setup(self, setup: FrameProcessorSetup): ... @releases("client") async def cleanup(self): ... ```(PR #5316)
-
Added
BaseObserver.on_processor_setup, called with aProcessorSetUponce each processor has been set up. Services connect during setup, so this is where that cost can be measured; processors are set up concurrently, so these arrive in the order they finish rather than in pipeline order.
(PR #5316) -
Added
on_setup_timeoutandon_pipeline_timeoutevents toPipelineWorker, so a pipeline that gives up waiting says so.on_setup_timeoutfires when the processors never finish setting up, and takes no frame, since none has been pushed yet.on_pipeline_timeoutfires when a frame the worker was waiting on never reaches the end of the pipeline: aStartFramethat never starts it, or aCancelFramethat never drains it, so inspect the frame to tell the two apart.@worker.event_handler("on_pipeline_timeout") async def on_pipeline_timeout(worker, frame): if isinstance(frame, StartFrame): ...
(PR #5316)
-
Added a
stop_on_failurefield to eval scenarios. It defaults totrue, the existing behavior, where the first turn with a failed assertion ends the scenario. Set it tofalsefor a scenario whose turns are scored independently — a benchmark reporting a per-turn pass rate needs every turn driven, not just the ones before the first miss.
(PR #5317) -
Added
TTFATMetricsData, reporting time to first answer token for LLM services that answer in text. It runs from the request to the first token the caller sees, excluding any reasoning streamed first, and carriesttfat,ttfb, andthinking_time(ttfatminusttfb) so the cost of a model thinking is readable from one metric. It reaches RTVI clients asttfatand is logged byMetricsLogObserver. Speech-to-speech services report nothing, having no answer token to measure to.
(PR #5320) -
Added
pcm_to_wav()topipecat.audio.utils, which wraps raw 16-bit PCM in a WAV container and returns the file as bytes. It takes PCM in the forms Pipecat pipelines carry it —bytes,bytearray, ormemoryview— so audio from anAudioBufferProcessorevent handler can be written or uploaded directly.
(PR #5326) -
Added
AudioBufferProcessoreventson_user_turn_audioandon_bot_turn_audio, which fire once when a turn ends with aTurnAudioDataholding that speaker's audio for the whole turn and the turn number. Turn tracking, which the pipeline worker enables by default, supplies the boundary and the number.
(PR #5329) -
Added per-turn results to
pipecat.evals.EvalResult.turnsholds anEvalTurnResultfor each turn in the scenario — itsstatus(passed,failed, ornot_run), the failures it produced, and its duration — so scoring a run turn by turn no longer means groupingEvalResult.failuresbyturn_indexand opening the scenario file for a denominator. A turn the run stopped before reaching reportsnot_runinstead of looking like a pass.pipecat eval suitewrites the same statuses to each run'sresults.jsonlline, andpipecat evalprints a2/4 turnstally for a run that drove every turn and failed some.
(PR #5332) -
Added
ElevenLabsDialogueTTSService, a WebSocket TTS service for ElevenLabs Eleven v3 models (eleven_v3andeleven_v3_conversational), whichElevenLabsTTSServicecan't reach. It needs workspace access to ElevenLabs' Text-to-Dialogue API.stabilityis the only voice setting Text-to-Dialogue reads, and text is always aggregated into sentences (TextAggregationMode.SENTENCES):from pipecat.services.elevenlabs.dialogue.tts import ElevenLabsDialogueTTSService tts = ElevenLabsDialogueTTSService( api_key=os.getenv("ELEVENLABS_API_KEY"), settings=ElevenLabsDialogueTTSService.Settings( voice=os.getenv("ELEVENLABS_VOICE_ID"), model="eleven_v3_conversational", ), )
Keep using
ElevenLabsTTSServicefor Flash, Turbo, and Multilingual models, which have lower latency and a fuller set of voice controls.
(PR #5353) -
Added a warning when a
thinking_budgetis set on a Gemini 3 model. Gemini 3 takesthinking_levelinstead. Passingthinking_budgetresults in ill-defined behavior (it may be honored, silently ignored, or rejected, depending on the model and the backend).
(PR #5356) -
Added
DeepgramFluxSageMakerTTSService, running Deepgram Flux TTS on a SageMaker endpoint. It takesendpoint_nameandregioninstead of an API key and accepts the same settings asDeepgramFluxTTSService, includingvoice,speedandexpressivity. Requirespipecat-ai[deepgram,sagemaker]and AWS credentials.
(PR #5360) -
Added support for Azure's v1 API surface, which Microsoft Foundry displays as the "Azure OpenAI endpoint".
AzureLLMServiceselects it wheneverendpointends in/openai/v1, andAzureRealtimeLLMServicereaches it from abase_urlwith no query string, appending the deployment named bySettings.model:llm = AzureLLMService( api_key=os.getenv("AZURE_CHATGPT_API_KEY"), endpoint="https://my-resource.openai.azure.com/openai/v1", settings=AzureLLMService.Settings(model="my-deployment"), ) realtime = AzureRealtimeLLMService( api_key=os.getenv("AZURE_REALTIME_API_KEY"), base_url="wss://my-resource.openai.azure.com/openai/v1/realtime", settings=AzureRealtimeLLMService.Settings(model="my-deployment"), )
AzureLLMServicestill serves dated endpoints, routing them throughapi_version. ForAzureRealtimeLLMService, v1 is the supported surface: endpoints carrying a datedapi-versionserve the superseded preview protocol, which rejects the session configuration and names its events differently, so they aren't usable here.
(PR #5363) -
Added
token_providertoAzureLLMServiceandAzureRealtimeLLMServicefor Microsoft Entra ID authentication, so Azure services can run without an API key.api_keyis now optional, and passing neither credential raisesValueError:from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider llm = AzureLLMService( token_provider=get_bearer_token_provider( DefaultAzureCredential(), "https://ai.azure.com/.default" ), endpoint="https://my-resource.openai.azure.com/openai/v1", )
(PR #5363)
-
Added
--ice-serversto the development runner, along with the matchingPIPECAT_ICE_SERVERSenvironment variable, so a bot started throughpipecat.runner.run.main()can gather candidates from custom STUN and TURN servers:python bot.py -t webrtc --ice-servers stun:stun.l.google.com:19302
An entry is a bare URL, or a JSON object with
urls,username, andcredentialwhen a TURN server needs authentication. The environment variable takes the same entries comma-separated or as a JSON array. Configured servers also reach WebRTC clients in theiceConfigof the/startresponse, so both peers negotiate against the same servers.
(PR #5376) -
Added
saaras:v4to the models supported bySarvamSTTService. It uses the same WebSocket contract assaaras:v3— the same modes and fine-grained VAD tuning parameters — and adds Global English alongside Indian English and the 22 Indic languages.
(PR #5382) -
Added client-side TLS options to the MoQ transport:
client_tls_cert/client_tls_keypresent a certificate to a relay that authenticates its peers with mTLS, andclient_tls_roots/client_tls_fingerprintsverify a relay behind a private CA or a self-signed one. The latter two are alternatives to switchingverify_ssloff, which was previously the only way to reach such a relay; a bot in serve mode already publishes its own fingerprints asMOQTransport.cert_fingerprintsfor a peer to pin.
(PR #5387) -
Added
BlandTTSService, realtime WebSocket text-to-speech using Bland, andBlandHttpTTSServicefor complete-text HTTP requests. Install withuv add "pipecat-ai[bland]".
(PR #5388) -
Added
max_consecutive_zero_audio_contextstoTTSService. A provider can accept every request and answer with silence — an unknown voice ID, say — without ever reporting an error, leaving the bot mute with nothing in the logs to explain it. Every TTS context that completes without producing audio reports an error the service can carry on from, so application code hears about a turn that produced no speech as it happens. After this many silent contexts in a row, the service reports a permanent error instead, stops being given work, and the pipeline worker applies itsProcessorUnusablePolicy(aServiceSwitcherfails over to another provider). Defaults to 3; set it to 0 to report silent contexts without ever writing the service off.
(PR #5393) -
Added
PipelineWorker(handle_flush_frame=...), which says whether a worker answers a flush probe. It defaults to whether the pipeline is unbridged, so a bridged worker takes part in the trip but leaves the answering to the pipeline that owns the bridge, which is what makesflush_pipeline()on a bridged worker wait for what it produced to reach the end of that pipeline rather than only for its own queues to empty. A bridged worker with no such peer never completes a flush.
(PR #5399) -
Added
BusSubscriber.accepts_bus_message(message), which the bus consults before every delivery to decide whether to hand the message to that subscriber. ReturningFalsedrops it for that subscriber alone; others still receive it. It accepts everything by default.
(PR #5399) -
pipecat eval runnow accepts a directory of.yamlscenario files and executes them in deterministic filename order.
(PR #5414) -
Added
"max"to the reasoning effort levelsOpenAIResponsesLLMService.ReasoningConfigaccepts, matching OpenAI's current set for the Responses API.
(PR #5432) -
Added
complete_marker,incomplete_short_markerandincomplete_long_markertoUserTurnCompletionConfig, so a bot can choose markers that are a single token in its own model's tokenizer. The turn completion instructions and both incomplete-turn re-prompts are rendered from whichever markers are configured.
(PR #5437) -
Added
GeminiSTTService, a streaming speech-to-text service using Google'sgemini-3.5-transcribe-livemodel over the Gemini Live API. Language is auto-detected by default;GeminiSTTService.Settingssupportslanguageshints andadaptation_phrasesto bias recognition toward domain-specific terms. Requires google-genai >= 2.9.0.The model detects utterance boundaries itself, and when the pipeline's VAD signals end of speech the service flushes the utterance so the final transcript arrives promptly instead of when the model decides the utterance ended.
(PR #5449)
Changed
-
⚠️ ExternalUserTurnStrategies, when driven by the new proposed turn frames, now pushesUserStartedSpeakingFrame/UserStoppedSpeakingFrameand broadcasts the interruption itself rather than leaving both to the service. Fed real turn frames it still emits nothing, so pipelines built around a sharedUserTurnProcessoror a third-party service that emits turn frames directly are unaffected.This matters if you pass
should_interrupt=Falseto a turn-detecting STT and pinuser_turn_strategies=ExternalUserTurnStrategies()by hand: the service carriesshould_interrupton the strategies it recommends, but a user-supplieduser_turn_strategiesdiscards that recommendation, so interruptions come back on. Drop the manualuser_turn_strategies— the service recommends the right strategies on its own now — or passExternalUserTurnStrategies(enable_interruptions=False). The aggregator logs a warning naming both fixes when it detects this.Relatedly, a turn-detecting service paired with pinned non-external strategies (e.g. VAD or turn analyzer strategies) no longer drives turns at all; the pinned strategies own them.
(PR #5156) -
Renamed
MOQParams.serve_bindtoMOQParams.bind, which now also sets the local source address a client-mode bot dials from.MOQRunnerArguments.serve_bindis renamed to match. The old name still works and warns; it will be removed in 2.0.0.
(PR #5158) -
MoonshineSTTServicenow resolves languages the way the other STT services do: aLanguagemaps to one of Moonshine's eight languages (Arabic, Chinese, English, Japanese, Korean, Spanish, Ukrainian, Vietnamese) throughlanguage_to_moonshine_language(), with regional variants such asLanguage.ES_MXresolving to their base code. A language Moonshine publishes no model for raises with the list of supported languages, instead of failing inside the model download.
(PR #5182) -
MoonshineSTTServicereloads its model whenlanguageormodelchanges at runtime (including viaset_language()). Previously the new value was stored but the loaded model kept transcribing in the old language. A failed reload keeps the loaded model and pushes anErrorFrame.
(PR #5182) -
OpenAI-compatible LLM services now report token usage once per completion. Providers that repeat a cumulative usage snapshot on every streamed chunk previously produced a token-usage
MetricsFramefor each one, over-counting a single turn for anything aggregating those frames.SambaNovaLLMServicealso now reports the cache-read and reasoning token counts its provider sends.
(PR #5190) -
GrokRealtimeLLMService'svoicesetting is typedstrrather than a fixed list of five names, and accepts any built-in Grok voice ID (xAI documents the catalogue at https://docs.x.ai/docs/guides/voice/agent) or a custom ID from the Custom Voices API. Voice IDs are case-insensitive.GrokVoiceis an alias ofstr.
(PR #5200) -
Behavior change: the default Grok Realtime voice is now
eve, the voice xAI documents as its default, instead ofAra. Anyone relying on the previous out-of-the-box voice should setvoice="ara"explicitly onSessionProperties.
(PR #5200) -
⚠️ Streaming STT services no longer report processing metrics —ProcessingMetricsDatainMetricsFrame, surfaced as theprocessingfield of RTVI'smetricsmessage. Nothing changes forSegmentedSTTServicesubclasses.Processing metrics time a discrete unit of work, and a streaming STT doesn't really perform one — audio arrives continuously. The 22 affected services' measurement methodologies were inconsistent and either not meaningful or duplicative of TTFB.
TTFB — speech end to final transcript — is the STT latency measure, and it is unaffected.
(PR #5209) -
⚠️ WebsocketTTSServicesubclasses andDeepgramSageMakerTTSServiceno longer report processing metrics, which were meaninglessly reporting zero on every turn. The metric isProcessingMetricsDatainMetricsFrame, surfaced as theprocessingfield of RTVI'smetricsmessage. TTS services whose processing time was a real number are unaffected.Processing time is measured around
run_tts. For a service that requests
audio and waits for it in that call, that covers the real work.
WebsocketTTSServicesubclasses instead push the text onto the socket and
return, leaving the audio to arrive on a separate receive task, so the
measurement only ever covered the send.DeepgramSageMakerTTSServicedoes
the same over bidirectional HTTP/2. TTFB and TTFA measure the latency that
matters for all of them, and are unaffected.There's a new
TTSService.supports_processing_metricsproperty, which
defaults toTrue. Set it toFalseon a custom service whoserun_tts
returns before synthesis finishes, or back toTrueon a
WebsocketTTSServicesubclass that waits for the server to signal the end.
(PR #5220) -
Changed
JobGroup.worker_namesfrom asetto alist, preserving the order the workers were dispatched in so anything rendering them, such as a client UI job-group card, stays stable across a group's lifetime.JobGroupalso now carries the group'slabelandcancellablesettings and the set of workers that have reached a terminal state.
(PR #5221) -
CartesiaSTTService'sbase_urlnow also accepts a URL carrying a scheme (ws://localhost:8000) rather than only a bare host, so the connection can be made over plainwsagainst a local or proxied endpoint instead of alwayswss.
(PR #5223) -
CartesiaTTSServicenow authenticates with theX-API-KeyandCartesia-Versionheaders on the websocket handshake instead ofapi_keyandcartesia_versionquery parameters, matching the Cartesia STT services.
(PR #5223) -
⚠️ calculate_audio_volume()now requires at least 400ms of audio, the length of an ITU-R BS.1770 gating block, and raisesValueErrorfor anything shorter. Code passing individual audio frames should useAudioVolumeTrackerinstead, which accumulates them into a rolling window. Volume is still reported on the same 0 to 1 scale, soVADParams.min_volumethresholds carry over unchanged.VAD continues to run on 32ms frames; only the volume measurement spans a wider window. Because loudness is now integrated over 400ms rather than a single frame, brief dips between phonemes no longer drop the measured volume below
min_volumemid-word.
(PR #5232) -
GoogleLLMServicenow defaults togemini-3.6-flash, up fromgemini-2.5-flash. 2.5 Flash follows the async-tool result-reporting instruction unreliably, and its failure mode is announcing a fabricated result rather than staying silent. SetmodelinGoogleLLMService.Settingsto pin the previous default.
(PR #5236) -
AICQuailVADAnalyzernow usesvad-2.1-xxs-16khzby default. The old default,quail-vad-2.0-xxs-16khz, does not work withaic-sdk3.0. If you setmodel_idyourself, pick a model listed at https://artifacts.ai-coustics.io/.
(PR #5239) -
The
aicextra now requiresaic-sdk~=3.0, which reworked its audio and VAD APIs. Upgrade the SDK when you upgrade pipecat;aic-sdk2.5.x no longer works.
(PR #5239) -
ServiceSwitchernow fails over only on errors that leave a service unable to do its job (is_usable=False), and reports its services' failures as its own.ServiceSwitcherStrategyFailoverswitches only once the active service reports an error that leaves it unable to do its job, rather than on any error, so a provider hiccup no longer costs a failover. It switches to the next service that is still usable, and a successful switch consumes the error: the switcher went on doing its job, so nothing upstream needs to act on it.The rest of the pipeline deals with the switcher rather than with the services inside it, so what it does with an error depends on which service reported it:
- From a service it isn't using: the error stops at the switcher, since a service held in reserve can't stop the switcher doing its job. Watch that service's own
on_usable_changedto hear about it. - From the active service, with somewhere to fail over to: consumed, as above.
- From the active service, with nowhere left to go: re-reported against the switcher itself, naming the service that failed.
The switcher's
is_usableis a reading of its services: it reports itself unusable only once none of them can work, so one service's rejected API key never writes off the switcher along with it. Bringing any service back withset_usable(True)brings the switcher back with it; calling that on the switcher itself does nothing, since it has no usability of its own to set. The switcher raiseson_usable_changedfor itself whenever that reading moves, so watching the switcher is enough to hear about the services inside it.
(PR #5242) - From a service it isn't using: the error stops at the switcher, since a service held in reserve can't stop the switcher doing its job. Watch that service's own
-
Websocket services now stop reconnecting once the service can no longer do its job, instead of retrying credentials the provider has already rejected.
Running out of reconnection attempts now leaves the service unusable too (
is_usable=False), so a connection that can't be re-established is reported as such rather than being retried on every subsequent request. Errors reported during reconnection carry the exception that caused them, so they can be classified.Giving up is reported through the
report_errorcallback, which takes an optionalforce_treat_as_permanentargument alongside the error frame. A service that overrides_report_errorshould accept and forward it.
(PR #5242) -
STT and TTS services now stop working once they can no longer do their job — a bad API key, an unknown model or voice, a connection that won't come back — instead of retrying for every chunk of audio or piece of text.
Previously a rejected API key on a service that connects on demand produced a connection attempt and an
ErrorFrameseveral times a second for as long as the pipeline ran.STTServiceandTTSServicenow skip transcription and synthesis while the service is unusable.Services whose credentials are signed or resolved per connection —
AWSTranscribeSTTServiceandNvidiaSageMakerTTSService— treat a rejected credential as recoverable, since reconnecting is what refreshes it.Pair this with
PipelineWorker(processor_unusable_policy=...)or anon_pipeline_errorhandler to decide what the bot should do about it.
(PR #5242) -
The eval judge now defaults to
gemma4:12bwithreasoning_effort: none, replacinggemma2:9b. Runollama pull gemma4:12bbefore running scenarios that use the default judge. The previous default mistook a short interim reply for a complete answer — a bot that had so far said only "Let me check on that." would satisfy the criterion, passing a turn in which the bot said nothing. To keep the old judge, set it explicitly in a scenario's judge block:judge: {eval: {service: ollama, model: gemma2:9b}}.
(PR #5243) -
Updated the default model for
DeepSeekLLMServicefromdeepseek-chattodeepseek-v4-flash. SetmodelinDeepSeekLLMService.Settingsto pin the previous default.
(PR #5246) -
Updated the default model for
MiniMaxHttpTTSServicefromspeech-02-turbotospeech-2.8-turbo. SetmodelinMiniMaxHttpTTSService.Settingsto pin the previous default.
(PR #5246) -
Updated the default model for
FishAudioTTSServicefroms2-protos2.1-pro. SetmodelinFishAudioTTSService.Settingsto pin the previous default.
(PR #5246) -
Updated the default model for
LmntTTSServicefromauroratoblizzard. SetmodelinLmntTTSService.Settingsto pin the previous default.
(PR #5246) -
Updated the default model for
AsyncAITTSServiceandAsyncAIHttpTTSServicefromasync_flash_v1.0toasync_flash_v1.5. SetmodelinAsyncAITTSService.SettingsorAsyncAIHttpTTSService.Settingsto pin the previous default.
(PR #5246) -
SpeechTimeoutUserTurnStopStrategyno longer overrides the deprecatedreset()hook. Turn detection throughUserTurnControlleris unaffected; code that calls.reset()directly on this strategy now reaches the inherited no-op instead — callhandle_user_turn_started()(turn start) orhandle_user_turn_stopped()(turn stop) instead.
(PR #5252) -
Changed the default model for
GrokRealtimeLLMServicetogrok-voice-latest, xAI's recommended Voice Agent alias. Pin a versioned model explicitly (e.g.settings=GrokRealtimeLLMService.Settings(model="grok-voice-think-fast-1.0")) for stability.
(PR #5255) -
AWS Nova Sonic's
AudioConfignow requires anintfor each of its sample-rate, sample-size, and channel-count fields, rejecting an explicitNoneat construction. Every field already defaults to a real value, and aNonethat reached session continuation — which sizes its audio buffer from them — raised aTypeErrorthere instead.
(PR #5273) -
⚠️ LLMSetToolsFrame.toolsno longer lists a bare list of provider-specific tool dicts among the forms it accepts. That form last worked throughOpenAILLMContext, which stored it verbatim, and stopped when that deprecated context was removed in 1.0.0. Provider-native tools travel in aToolsSchema'scustom_tools, keyed by adapter type — a form the frame already carries end to end, into a realtime service's session update included. Nothing changes at runtime: frames are dataclasses and don't validate.
(PR #5273) -
The async function-calling examples no longer set
enable_async_tool_cancellation=True, so they demonstrate async tools on their own. Cancellation asks the model to judge whether a pending result is still wanted, and a model that judges too readily cancels a result the user was waiting on and never mentions it — which is worth knowing before turning it on, and is now noted on the parameter itself.
(PR #5278) -
⚠️ Removed Arcana model support from Rime TTS services before Rime's cloud cutoff on August 15, 2026 at 12:00 UTC. Setmodel="coda"when you upgrade. Rime examples use Luna for cross-model voice continuity.
(PR #5279) -
⚠️ ChangedSarvamLLMServicedefaultbase_urlfromhttps://api.sarvam.ai/v1to be resolved automatically from the selected model:https://api.sarvam.ai/v1forsarvam-105b-conversations,https://api.sarvam.ai/v2for all other models. Existing users who relied on the/v1default withsarvam-105bmust passbase_url="https://api.sarvam.ai/v1"explicitly or update to/v2.
(PR #5288) -
FunctionCallCancelFramecarries arun_llmfield, defaulting to False.LLMServicesets it only when a call is cancelled by its own timeout — an interruption must not trigger inference, and a cancellation the LLM requested already runs inference through the result of the tool that requested it.LLMAssistantAggregatorpushes the context upstream when the flag is set, holding off while sibling calls from the same LLM response are still in flight so the group still triggers inference exactly once.
(PR #5291) -
⚠️ A function call that exceedsfunction_call_timeout_secs(or a per-tooltimeout_secs) is now cancelled rather than left to run: its handler is thrown anasyncio.CancelledErrorso it can clean up, and the call settles through the path interruptions and LLM-requested cancellation already use — aFunctionCallCancelFrameand theon_function_calls_cancelledevent — then runs inference so the bot reports that the call didn't complete. Previously the deadline reported an empty result while the handler kept running, so its side effects still landed and its real result was discarded. The deadline covers the handler's own execution; work it spawns into a task of its own is not cancelled with it.
(PR #5291) -
⚠️ SonioxTTSServicenow defaults to Soniox'stts-rt-v2model, withBryceas the default voice.tts-rt-v2speaks the same WebSocket API astts-rt-v1but offers a different roster of voices, so avoiceset explicitly must be onetts-rt-v2offers. Soniox removestts-rt-v1on August 31, 2026, after which requests naming it route totts-rt-v2regardless.
(PR #5295) -
DeepgramFluxTTSServicenow cancels the active turn with Flux'sInterruptmessage instead of reconnecting the websocket, so the cross-turn acoustic state that keeps a voice consistent survives a barge-in.Because an interruption no longer closes the connection,
on_connectedandon_disconnectedstop firing on every barge-in.
(PR #5296) -
Changed how a
function_callexpectation matches arguments inpipecat.evals. A turn expectingargs:now passes if any call of that name matches them, where before it checked only the first call sharing the name and failed there. An LLM that gets a call wrong and immediately repeats it correctly now satisfies the turn, and when nothing matches, the failure names the arguments that did arrive.
(PR #5314) -
A processor holds every frame it receives until its
StartFramearrives. A service that connects during setup can push frames before the pipeline starts; those frames now wait and are delivered after theStartFrame, in arrival order, so a processor never acts on a frame before it has started.
(PR #5316) -
StartupTimingObservermeasures a startup that now happens mostly before theStartFrame, so its report covers setting up as well as starting.ProcessorStartupTiming.duration_secsis what a processor cost to get ready, itssetup()andstart()together, so it keeps reporting the same magnitude now that connecting has moved intosetup(). The newsetup_duration_secsbreaks out the connecting part.StartupTimingReport.total_duration_secsis the span from the pipeline starting to set up until it had started, rather than the sum of what each processor cost. Processors are set up concurrently, so a sum would report a pipeline as slower the more of its work overlapped.TransportTimingReport.bot_connected_secsandclient_connected_secsrun from the pipeline starting to set up, so they measure the real time to a connected bot. A transport that connected before theStartFramewas pushed previously went unreported.
(PR #5316)
-
The pipeline clock now runs from the moment the pipeline starts setting up rather than from the
StartFrame, so frames pushed while processors connect are no longer timestamped zero. Presentation timestamps therefore start at roughly what setting up cost; everything comparing them does so relatively, so pacing and playback are unaffected.
(PR #5316) -
Pipeline configuration reaches processors through
FrameProcessorSetupinsetup()rather than throughStartFrame.setup.audio_in_sample_rate,setup.audio_out_sample_rate,setup.enable_metrics,setup.enable_tracing,setup.enable_usage_metrics,setup.report_only_initial_ttfbandsetup.tracing_contextare available fromsetup()onwards, which is what lets a custom processor connect or resolve sample rates there.
(PR #5316) -
GroqLLMServicenow defaults toopenai/gpt-oss-120b. The previous default,llama-3.3-70b-versatile, is being retired by Groq. Passsettings=GroqLLMService.Settings(model=...)to choose a different model.
(PR #5338) -
⚠️ UltravoxRealtimeLLMServiceandVonageVideoConnectorTransportno longer cancel the pipeline when their connection fails. They now report the failure as one that leaves the service unusable, so the pipeline follows theprocessor_unusable_policyitsPipelineWorkerwas given — by default it keeps running and the application decides what to do. Passprocessor_unusable_policy=ProcessorUnusablePolicy.CANCELto keep the previous behavior.
(PR #5348) -
⚠️ GoogleVertexLLMServicenow defaults togemini-3.6-flash, and its defaultlocationchanged fromus-east4toglobal, because Vertex serves the Gemini 3 series only from the global endpoint. Passsettings=GoogleVertexLLMService.Settings(model="gemini-2.5-flash")andlocation="us-east4"to keep the previous configuration.
(PR #5356) -
DeepgramFluxSTTBasemoved topipecat.services.deepgram.flux.stt_base.
(PR #5360) -
OpenAI Realtime sessions now use
gpt-realtime-2.1as the default model.
(PR #5362) -
AzureLLMServicenow routes endpoints outside the v1 API surface through2025-04-01-preview, the last dated version Azure issued, so recent Azure features are available without naming a version.
(PR #5363) -
The
cliextra now requires Pipecat Context Hub 0.5.3 or newer, so a plainpipecat-ai[cli]install can runpipecat context-hub refresh --framework-version latest— the refresh the agent guides written bypipecat initprescribe. It pins the index to the newest releasedpipecat-aitag instead ofmain, and re-resolves on every run, so a later incremental refresh picks up a new release without--force.
(PR #5367) -
Updated the MoQ transport to
moq-rs0.4. Broadcasts are now created on an origin rather than constructed standalone, the track subscriptions (subscribe_catalog,subscribe_audio,subscribe_json_stream) are awaited, andMoqErrorisError. The publish broadcast and transcript track are still created synchronously in__init__, so the bot loses no startup audio.Fixed the MoQ transport reporting a normal peer hangup as an error. A peer that vanishes mid-call drops its producer without finishing, which moq-rs raises with the reason as the message tail rather than as a reset code, so the hangup classifier missed it and the disconnect surfaced through
on_errorwith a traceback.
(PR #5378) -
SarvamSTTServicenow usessaaras:v4as its default model instead ofsaaras:v3. Applications that relied on the previous default should setsettings=SarvamSTTService.Settings(model="saaras:v3")explicitly.
(PR #5382) -
A TTS context that completes without producing any audio now resumes frame processing as soon as that is known, instead of leaving it paused until the pause watchdog fires a few seconds later. The non-fatal
ErrorFramethe watchdog reports no longer accompanies these silent turns.
(PR #5393) -
TTSServicewithpause_frame_processing=Truenow pauses only while there is audio to wait for: the bot speaking, or an audio context still open that may yet produce audio. Previously a turn that produced no audio could stall the pipeline for a few seconds until a watchdog force-resumed it and reported a non-fatal error.
(PR #5394) -
The example bots set
processor_unusable_policy=ProcessorUnusablePolicy.END, so an example ends once one of its processors can no longer do its job — a rejected API key or an unknown model, say — instead of running on with a service that will keep failing.
(PR #5397) -
Changed
PipelineWorker.end()andPipelineWorker.activate_worker()to wait for in-flight frames before they go through, so a closing line is heard rather than cut off and a worker handing over stops talking before the one taking over starts. Previously onlyLLMWorkerarranged this. A pipeline that never started, or one that has already finished, is left alone. Cancelling still takes effect immediately.
(PR #5399) -
Changed
WorkerRunnerto send each worker one shutdown message instead of two.cancel()now signals shutdown and the messages go out as the runner exits, carrying the reason the caller gave rather than a generic one, and addressed only to workers that have not already finished.
(PR #5399) -
Changed
BaseWorker(active=...)so that it governs whether a worker accepts bus messages at all. It previously gated only the frames a bridged worker received, leaving job requests, UI events and every other kind of bus traffic to arrive whatever the worker's state. An inactive worker is now handed only activation, deactivation, end or cancel messages. Nothing else reaches it, so noon_bus_messageoverride oron_bus_messageevent handler runs for it either, which includes aBaseUIWorkerno longer honouring the client's__cancel_job_groupwhile inactive.@worker_readyhandlers are unaffected, since they fire from theWorkerRegistryrather than over the bus.
(PR #5399) -
Changed
PipelineWorker.flush_pipeline()to wait for as long as the pipeline keeps working. Itstimeoutnow counts seconds without progress rather than seconds in total, so a long turn keeps the wait alive while a stuck pipeline still gives up promptly. Progress is a frame reaching the sink, or a report from the pipeline answering the probe when it crossed into another worker. Heartbeats are not counted. A caller that gives up now says what it did: settled a function call before its output was delivered, or handed over without draining.
(PR #5399) -
KrispVivaSDKManagernow keeps the Krisp VIVA SDK initialized for the life of the process:release()no longer callskrisp_audio.globalDestroy(), andis_initialized()staysTrueafter the last reference is released. Native sessions are still released per component, so per-call memory is unchanged. One consequence is thatapi_keyis read only by the call that initializes the SDK, so a process serving sessions under different Krisp licenses uses the first one for all of them.
(PR #5411) -
The
moonshineextra now requiresmoonshine-voice>=0.1.5, up from>=0.0.62. Existing installs needuv sync(orpip install -U "pipecat-ai[moonshine]") to pick the new version up.
(PR #5422) -
Updated the
runnerextra to requirepipecat-ai-prebuilt>=1.0.6, refreshing the prebuilt client UI served by the development runner with@pipecat-ai/client-react1.8.2,@pipecat-ai/moq-transport0.1.1, and@pipecat-ai/voice-ui-kit0.13.1.
(PR #5427) -
AnthropicLLMService.ThinkingConfignow covers Anthropic's current thinking API:type="adaptive", the mode Claude 4.7 and later models require, anddisplay, which asks for summarized thinking text on models that omit it by default.
(PR #5429) -
A session now tears its controllers and its input audio filter down once, instead of once from the
EndFrameorCancelFramehandler and again fromcleanup().
(PR #5434) -
VADControllerandUserTurnControllergained astart(), called by their owner, and they andUserIdleControllergained astop().BaseAudioFilter.start()is now called from the input transport'ssetup()rather than onStartFrame.
(PR #5434) -
⚠️ Changed the user turn completion markers to a fill gradient:●marks a complete turn (previously✓),◐a turn cut off mid-thought (previously○), and○a user who needs more time (previously◐). The two incomplete markers have swapped meaning, so a customUserTurnCompletionConfig.instructionsstring or a model fine-tuned on the old markers now maps short and long waits the wrong way round; setcomplete_marker,incomplete_short_markerandincomplete_long_markeronUserTurnCompletionConfigto keep the previous characters. Every marker is now a single token in every major tokenizer, and since the complete marker is generated before any speakable text, this removes up to two decode steps from the bot's first spoken word.
(PR #5437) -
Changed
PipelineWorker.flush_pipeline()to also wait for work the pipeline starts by pushing upstream, such as the LLM run a function call result triggers. The probe used to turn around at the source and settle there, returning before that response had been generated, let alone rendered; it now travels down, up, and down again, settling on the second arrival at the sink.
(PR #5438) -
Changed
PipelineWorker.activate_worker()to drain the pipeline only whendeactivate_selfis set. A worker that stays active is handing nothing over, so there is nothing in flight to wait for, and waiting meant the first activation of a session blocked on the very worker it was about to wake.
(PR #5438) -
pipecat initnow scaffolds Cartesia TTS with a voice recommended forsonic-3.5, the service's default model. Examples use the same voice.
(PR #5441) -
GradiumSTTServicenow defaultslanguagetoLanguage.ENinstead of leaving it unset. Grounding the model to a language improves transcription accuracy. Setsettings=GradiumSTTService.Settings(language="any")to have Gradium detect the language instead.
(PR #5444) -
AnthropicLLMServicenow disables thinking by default on Sonnet 5 and later, where adaptive thinking is otherwise on and the model decides per request whether to think, to keep latency low for real-time voice — mirroring how the Gemini service disables thinking by default on Flash models. Opus and Fable are left at Anthropic's default. SetSettings.thinkingto configure thinking explicitly.
(PR #5446) -
CerebrasLLMServicenow sends "developer"-role messages unchanged instead of converting them to "user" messages. Cerebras maps the role to its developer instruction layer, which sits above user instructions in the prompt hierarchy.
(PR #5448) -
MoondreamServicenow defaultsrevisionto2025-06-21instead of2025-01-09, picking up the newer Moondream build. Passrevision="2025-01-09"to stay on the previous one.
(PR #5458)
Deprecated
-
Deprecated
MCPClientmethodsregister_tools(),register_tools_schema(), andget_tools_schema(). UseMCPClient.tools()instead.
(PR #4939) -
Deprecated the
enable_user_speaking_framesconstructor parameter onBaseUserTurnStartStrategyandBaseUserTurnStopStrategy, which will be removed in 2.0.0. Whether a turn is announced is a per-turn decision rather than a per-strategy setting: passenable_user_speaking_framestotrigger_user_turn_started()/trigger_user_turn_stopped()where the strategy decides the turn. Passing it to a constructor still applies and now emits aDeprecationWarning.ExternalUserTurnStartStrategyandExternalUserTurnStopStrategysuppress emission on their own whenever the turn was already announced elsewhere — by a sharedUserTurnProcessor, or by a service that emits turn frames rather than proposing them — so a pipeline built on those strategies doesn't need to set the flag anywhere.
(PR #5156) -
Deprecated
UIWorker.ui_job_group(),UIWorker.start_ui_job_group(), andUIJobGroupContext(all removed in 2.0.0): usejob_group(...)/request_job_group(...)/JobGroupContextinstead, since every group aBaseUIWorkerdispatches is client-visible. The deprecated wrappers keep their historical signatures and behavior in the meantime.
(PR #5221) -
Deprecated passing
name,payload,timeout, andcancel_on_errordirectly toBaseWorker.job(),job_group(),request_job(),request_job_group(), andcreate_job_group_and_request_job()(removed in 2.0.0). Passparams=JobParams(...)orparams=JobGroupParams(...)instead. The individual arguments keep working in the meantime, and passing both raisesTypeError.
(PR #5221) -
Deprecated the
cartesia_versionparameter ofCartesiaTTSServiceandCartesiaHttpTTSService. Both services send theCartesia-Versionheader they are written against, since their request payloads and response handling are tied to that version. Passingcartesia_versionwarns and still overrides the header until it is removed in 2.0.0.
(PR #5231) -
Deprecated
enable_async_tool_cancellationon LLM services; it will be removed in 2.0.0. Setcancellable_by_llm=Trueon the tools that should be cancellable instead. It still works meanwhile, treating every async tool as cancellable — which is worth moving off, because a model that wrongly decides a pending result is unwanted destroys work the user asked for, and a tool that never opted in can't have that happen to it.The flag's shape has changed with it: where it used to advertise a single generic cancel tool, it now advertises a
cancel_<name>for every async tool, so the tool set a model sees grows with the number of async tools registered.```python # Before llm = OpenAILLMService(api_key=..., enable_async_tool_cancellation=True) @tool_options(cancel_on_interruption=False) async def write_report(params: FunctionCallParams, topic: str): ... # After llm = OpenAILLMService(api_key=...) @tool_options(cancel_on_interruption=False, cancellable_by_llm=True) async def write_report(params: FunctionCallParams, topic: str): ... ```(PR #5304)
-
Deprecated
StartFrame.audio_in_sample_rate,StartFrame.audio_out_sample_rate,StartFrame.enable_metrics,StartFrame.enable_tracing,StartFrame.enable_usage_metrics,StartFrame.report_only_initial_ttfbandStartFrame.tracing_context, which will be removed in 2.0.0. Read the same values fromFrameProcessorSetupinsetup()instead. The fields still carry the pipeline's configuration, so a processor that reads one keeps working and emits aDeprecationWarning, once per call site.
(PR #5316) -
AudioBufferProcessor'son_user_turn_audio_dataandon_bot_turn_audio_dataare deprecated and will be removed in 2.0.0. They report a run of speech at a time, so one turn produces several and none carries a turn number. Useon_user_turn_audioandon_bot_turn_audioinstead.
(PR #5329) -
Deprecated "fatal" errors. Concretely, deprecated 3 things:
ErrorFrame.fatal, thefatalargument ofFrameProcessor.push_error(), andFatalErrorFrame, all of which will be removed in 2.0.0. A fatal error would cancel the pipeline outright; that's now an application decision. Passingfatal=Truestill cancels the pipeline, but now also emits aDeprecationWarning. There are two alternatives to fatal errors, depending on what your error means:-
The error leaves its originating processor unable to do any more work: report it with
push_error(..., force_treat_as_permanent=True). That marks the processor unusable, andPipelineWorkerapplies itsprocessor_unusable_policyto specify how to handle the resulting error. -
The error isn't about any processor's state, but the pipeline should stop anyway: push a regular
ErrorFrame(withoutfatal) and follow it with anEndWorkerFrame, which ends the pipeline after queued frames drain. UseCancelWorkerFrameinstead to abandon the queued frames, asfatal=Truedid.
(PR #5348)
-
-
Deprecated the
api_versionconstructor parameter onAzureLLMService, which will be removed in 2.0.0. Pointendpointat Azure's v1 API surface instead, by ending it in/openai/v1. Azure issued no dated version after2025-04-01-preview, and new features reach only the v1 surface. Endpoints outside that surface still route through2025-04-01-preview; passingapi_versionexplicitly still applies and now emits aDeprecationWarning.
(PR #5363) -
Deprecated the
pause_watchdog_timeout_sparameter ofTTSService, which will be removed in 2.0.0. Passing it warns and does nothing: a pause is now only taken while audio is playing or still on its way, so it is always lifted by theBotStoppedSpeakingFramethat follows playback or by the audio context completing in silence — no timer is needed to break it.
(PR #5394) -
Deprecated the
target_taskparameter ofBusBridgeProcessor. Usetarget_workerinstead; a "task" is an asyncio task and the thing being named here is a worker. Passingtarget_taskstill works and emits aDeprecationWarning. It will be removed in 2.0.0.
(PR #5438) -
Deprecated the
messagesandresult_callbackparameters ofLLMWorker.end()andLLMWorker.activate_worker(). Deliver the function call result from the tool handler instead, withawait params.result_callback(result), and the output it triggers is delivered before the worker ends or hands over. Passing either parameter still works and emits aDeprecationWarning. They will be removed in 2.0.0.
(PR #5438)
Removed
-
Removed
AICVADAnalyzer,AICFilter.create_vad_analyzer(), andAICFilter.get_vad_context().aic-sdk3.0 removed the energy-based VAD all three relied on. The first two were deprecated since 1.4.0;get_vad_context()was not, so calls to it need replacing withAICQuailVADAnalyzer, which runs a dedicated VAD model.
(PR #5239) -
Removed the sunset
saarika:v2.5andsaaras:v2.5models fromSarvamSTTService, leavingsaaras:v3andsaaras:v4as the supported models; applications pinned to either should move tosaaras:v4. The service now always connects to the transcription endpoint, sincespeech_to_text_translate_streamingonly servedsaaras:v2.5— translation is still available on the remaining models throughmode="translate".
(PR #5383) -
⚠️ Removed thepromptsetting and theset_prompt()method fromSarvamSTTService. Both were only ever honored bysaaras:v2.5, which Sarvam is sunsetting, so there is no replacement — code passingSarvamSTTService.Settings(prompt=...)should drop the argument.
(PR #5383)
Fixed
-
Fixed deadlock caused by
FrameProcessorResumeFramewaiting in the process queue by changing it to aSystemFrame.
(PR #3448) -
Made
GoogleLLMService,GoogleVertexLLMService, andGeminiLiveLLMServicemore resilient to a tool's JSON schema using a construct Gemini doesn't accept. Gemini supports only a limited subset of JSON Schema, and a single tool with an unsupported construct would fail the entire request.GeminiLLMAdapternow tries to convert the tool schemas into the supported subset before the request, logging each change it makes:- Vendor extensions (
x-prefixed keys, such as thex-mcp-headerGitHub's MCP server attaches to most of its tool properties) are dropped, joining theadditionalPropertiesalready stripped. - A union
type, such as["string", "number"], becomes the equivalentanyOf. - An
enumwhose members aren't strings is dropped, losing its constraint.
(PR #4939)
- Vendor extensions (
-
Fixed
MCPClientmethodsstart()andtools()hanging indefinitely when a server refused the connection. A failing transport cancels the connecting task from inside its own task group, and that cancellation went uncaught, leaving the connection result unsettled. The underlying error is now raised to the caller.
(PR #4939) -
Fixed
OpenAIResponsesHttpLLMServiceproducing a silent, empty turn when the Responses API reported aresponse.failed,response.incomplete, orerrorevent mid-stream. These events arrive on an otherwise healthy stream, so nothing raised and noErrorFramewas pushed, leavingServiceSwitcherStrategyunable to fail over and the failure absent from logs. They now push anErrorFrame, matching the WebSocket variant.
(PR #5141) -
Fixed a reconnect that could be deferred forever on an STT service with built-in turn detection.
STTServicedefers a reconnect requested while the user is speaking and re-enables it onUserStoppedSpeakingFrame, but a service that emitted that frame itself never received one — a broadcast doesn't reach its own emitter — so with a VAD analyzer in the pipeline the deferred reconnect never fired. These services now propose turn boundaries and the user aggregator emits the turn frames, which do reach the service.
(PR #5156) -
Fixed the MoQ transport reporting an ordinary hangup as a transport failure. A peer disconnecting resets every in-flight track subscription, which surfaces as a per-track error carrying a numeric remote code — distinct from the session-level WebTransport close the transport already recognised. A browser leaving mid-call drops its microphone producer without finishing it, so the bot's audio subscriber saw a
Droppedreset and logged anERRORplus a traceback and invokedon_error, for what is just the end of the call. Peer-gone reset codes are now treated as a normal close, like the session-level one.- Constrained the MoQ extra to
moq-rs~=0.3.2. The previous<1.0.0bound bought nothing against a hand-versioned pre-1.0 library: 0.4.0 renamedMoqErrortoError, replacedOriginProducer.publish()withcreate_broadcast(), and made thesubscribe_*helpers async, so a fresh install resolved to a release the transport can't run on. - Fixed the MoQ transport dropping its producers instead of finishing them on disconnect. Finishing the audio track flushes samples still inside the encoder, and finishing the broadcast unannounces it — dropped, it gets lingered instead, so the relay kept advertising a dead bot after every call.
(PR #5158)
- Constrained the MoQ extra to
-
Fixed
WhisperSTTServicesilently transcribing in English when its model can't handle the configured language. The English-only models — every.enone, including the defaultdistil-medium.en— accept any language and transcribe as English regardless, soSettings(language=Language.ES)produced fluent-looking English rather than an error. Constructing such a pairing now raises aValueErrornaming the model and its supported languages; a mid-call switch viaSTTUpdateSettingsFramereports a non-fatalErrorFrameinstead, leaving the pipeline running.⚠️ Code that set a non-Englishlanguageon an English-only model was getting English transcripts and now raises at construction. Use a multilingual model (e.g.large-v3-turbo) or drop thelanguage.
(PR #5171) -
Fixed
KokoroTTSServicefailing to synthesize French and Mandarin. kokoro-onnx phonemizes through espeak-ng, which has nozhand no barefrvoice, so both raisedlanguage "..." is not supported by the espeak backendat synthesis time. Mandarin (including thezh-CN/zh-HK/zh-TWvariants) now maps tocmnand French tofr-fr, withfr-be,fr-chandpt-brmapped to the regional espeak-ng voices they have.
(PR #5171) -
Fixed
MoonshineSTTServicefailing to construct for any non-English language. Moonshine publishes its streaming architectures for English only and most other languages ship a single model, so the defaultsmall-streamingarchitecture didn't exist for, say, Spanish. An architecture unavailable for the configured language now falls back to the best model published for it.
(PR #5182) -
Fixed services surviving pipeline teardown and reconnecting as orphans.
TaskManager.cancel_task()absorbed everyCancelledErrorraised while awaiting the task it had cancelled, including the calling task's own cancellation. Because asyncio delivers a cancellation only once, a service tearing down from afinallyblock —DeepgramSTTService._connection_handlercancelling its keepalive, for example — never learned it had been cancelled, and as a reconnect loop went on reconnecting unsupervised.cancel_task()now re-raises a cancellation delivered to the caller while it waits, and still absorbs the cancelled task's own.
(PR #5186) -
Fixed
expand_unitsreading a quantity of one with a plural unit, so "Only 1km left" now becomes "Only 1 kilometer left" instead of "Only 1 kilometers left". A decimal such as "1.0km" keeps the plural.
(PR #5205) -
Fixed
ElevenLabsRealtimeSTTServicepushing two finalTranscriptionFrames per utterance wheninclude_language_detectionwas enabled withoutinclude_timestamps.
(PR #5208) -
Fixed
expand_numbersdropping a decimal's trailing zero, so "1.0" now reads as "one point zero" instead of the bare "one". This was most audible composed withexpand_units, which keeps the plural for a decimal:VoiceFormatter(expand_numbers=True)turned "1.0km left" into "one kilometers left". Decimals without trailing zeros are unchanged.
(PR #5213) -
Fixed
ExotelFrameSerializersending the stream identifier asstreamSidon outboundmediaandclearevents. Exotel's media stream protocol spells itstream_sid. Exotel treats the identifier as optional on messages from the bot, so existing integrations were unaffected.
(PR #5219) -
Fixed an async function call's result going unreported when the conversation moved on while the call was still running. A tool registered with
cancel_on_interruption=Falsekeeps running after the LLM's turn ends, so by the time its result arrives the user has often changed the subject — and the LLM would answer the new topic without ever mentioning the result. The final-result message now instructs the model to finish responding to whatever the user is talking about and then deliver the result at the end of that response, stating a short result outright and naming a long one with an offer of the details.
(PR #5236) -
Fixed
push_error_frame()raising an unrelatedIndexErrorin place of the error being reported, when that error carried an exception that was never raised and so had no traceback to read.
(PR #5242) -
Fixed
DeepgramSTTServicedropping the speaker's first word or two when someone is already talking as a session starts. The connection is established in the background, so audio arriving before there was a connection to carry it was discarded. Frames now wait at the service until the connection can carry them, and are transcribed in full once it can.
(PR #5254) -
Fixed
GrokRealtimeLLMServicedropping xAI Voice Agent server events that were not registered in the parser (notablysession.createdon every connect). The service now parses the full documented server event set, pushes interim user captions fromconversation.item.input_audio_transcription.updated, and handles text-modality deltas fromresponse.text.delta/response.output_text.delta.- Fixed
GrokRealtimeLLMServicesilently dropping user audio while conversation seeding was pending. Audio now flows aftersession.updated, so audio-only pipelines work without an explicitLLMRunFrame/_create_response. - Fixed interruptions under server VAD not cancelling the in-flight response on the wire.
InterruptionFramenow always sendsresponse.cancel; the input buffer is cleared only in manual turn mode so interrupting user speech is preserved. - Fixed
GrokRealtimeLLMServiceinterruptions only clearing local audio state. Interruptions now also sendconversation.item.truncateso server-side conversation history matches what the user heard.
(PR #5255)
- Fixed
-
Fixed
CartesiaTTSServiceandSonioxTTSServicedropping an already-heard sentence prefix from the transcript when a voice/model/language (or, for Soniox, speed) settings change was applied mid-sentence. The re-mint of the turn context now finalizes the old context's pending sentence first — so word-timestamps arriving during the flushed playout still emitAggregatedTextProgressFrames — mirroring the existing end-of-turn andTTSSpeakFrameclose paths.
(PR #5257) -
Fixed eval turns matching — and judges ruling on — output the bot produced for an earlier turn. Events queue up between turns and the matcher starts consuming as soon as a turn's input is sent, so whatever was already waiting was read first; a turn with
send_aftermade the window seconds wide. A turn that sends input now drops the queued bot output first. Turns that send nothing are observation-only and exist to match exactly that pending output, so they keep it.
(PR #5260) -
GoogleLLMServicenow closes a Gemini stream it stops consuming, so an interrupted or timed-out response releases its HTTP resources right away instead of waiting on garbage collection.
(PR #5262) -
Fixed
PatternPairAggregatorandSkipTagsAggregatormishandling an LLM response that ends with an unclosed start tag.PatternPairAggregator.flush()no longer leaks REMOVE-pattern content to TTS: it cuts at the earliest truly-unmatched REMOVE/AGGREGATE start delimiter (keeping unclosed KEEP content verbatim) and trims a trailing partial start delimiter.SkipTagsAggregator.flush()in TOKEN mode now returns buffered text instead of silently dropping it.
(PR #5266) -
Fixed
PatternPairAggregatorandSkipTagsAggregatorin TOKEN mode mishandling a start delimiter split acrossaggregate()calls: a trailing partial start delimiter is now held back until the next chunk completes it instead of being flushed (and spoken) as plain text. Also fixedSkipTagsAggregatorlosing track of its tag-scan position after a TOKEN-mode yield, which made every tag after the first closed one go undetected.
(PR #5268) -
Fixed
OpenAIResponsesHttpLLMServicerunning a function call with fabricated empty arguments when the stream ended in a terminal error (response.failed,response.incomplete, orerror) before the call's arguments finished streaming. Calls whose arguments did finish streaming still run.
(PR #5270) -
Fixed
GoogleTTSServiceandGoogleHttpTTSServiceraisingTypeErroron a settings update that setspeaking_ratetoNone.Noneis the field's default and the way to leave the rate to Google, but the range check these services run on an incoming rate handed it tofloat(). ANonerate now skips the range check.
(PR #5273) -
Fixed
InworldRealtimeLLMServiceraisingValueErrorwhen its input or output audio format was PCMU or PCMA. On every start the service syncs the configured format's sample rate with the transport's, and the G.711 formats are fixed at 8000 Hz and declare no rate to write to. The sync now applies only to the PCM format, the one with a configurable rate.
(PR #5273) -
Fixed
SpeechmaticsSTTServiceraisingAttributeErrorwhen constructed with an English locale it has no output-locale mapping for, such asLanguage.EN_IN. Such a locale is meant to log a warning and fall back to the base language code, but composing that warning was itself what raised. Construction now succeeds and the fallback is logged.
(PR #5273) -
Fixed
GeminiTTSServiceraisingAttributeErroron a settings update typed as the baseTTSSettingsrather thanGeminiTTSService.Settings. The service readsmulti_speakerandpromptoff the delta to warn about settings its GenAI backend ignores, and those fields exist only on its own settings type. It now reads them only when the delta carries them, as the sibling Google TTS services already do.
(PR #5273) -
Fixed
SimliVideoServiceraisingAttributeErrorwhen usingis_trinity_avatar=Truedue to its calling a nonexistent method—playImmediate—on the Simli client. The intended method is calledsendImmediate.
(PR #5273) -
Bots with async tool cancellation enabled now emit the
cancel_async_tool_callcall rather than only acknowledging the cancellation out loud. The instructions given to the LLM state that the call is the only thing that stops the pending work, so a bot that says it will skip a result no longer has that result arrive moments later and contradict it.
(PR #5276) -
enable_async_tool_cancellation=Truenow takes effect for bots that declare their tools through anLLMContext, which covers the direct-function andFunctionSchemahandler patterns. Previously the built-incancel_async_tool_calltool was never advertised to the LLM in that case — setup ran before those handlers were registered — so a bot could not cancel an async function call whose result the user no longer wanted, however clearly they asked for it. Setup no longer depends on a handler being registered before the pipeline starts.
(PR #5276) -
Fixed an async function call being made a second time, with a fabricated result, while the first was still running. The message announcing the call to the model described the message its result would arrive in — the role, the fields, how many there might be — and a model told the shape of a message it should expect tries to produce one, through the only structured channel it has: another function call, carrying the protocol payload as its arguments. The announcement now says only that the task is running, that its result will be given to the model, and that it should neither call again nor answer from nothing. Most visible on
GoogleLLMService, where the description named a developer-role message that the Gemini adapter rewrites to a user message, so the shape it described never arrived at all.
(PR #5277) -
An async function call's result is now reported reliably when the conversation has moved on, and reported after the answer to whatever the user last asked rather than ahead of it. A bot registering a tool with
cancel_on_interruption=Falsegets standing guidance in its system instruction — a result that has arrived is owed to the user, it belongs at the end of the reply that answers them, and it is said once. The per-result message carried the same policy, but it arrives buried in a context whose most recent turn is the user asking for something else, and a model weighing the two would answer and leave the result unsaid, or state it before the answer.
(PR #5278) -
Fixed a function call whose handler raises never being settled. The exception was reported upstream as a non-fatal
ErrorFrameand then nothing else happened, so the call stayed in progress forever:has_function_calls_in_progressnever cleared, sibling calls from the same LLM response could no longer complete their group, and aFunctionCallUserMuteStrategyorUserIdleControllercounting the call never saw it finish. The call now settles with a result reporting that the function failed, so the LLM can tell the user; the exception stays on theErrorFrameand out of the LLM context.
(PR #5291) -
Fixed a cancelled async function call (registered with
cancel_on_interruption=False) never being settled in the LLM context. It stayed in progress forever, sohas_function_calls_in_progressnever cleared and inference was suppressed for the rest of a parallel tool-call group. Cancelling one now settles it the way synchronous tool calls settle.
(PR #5291) -
Fixed a late result from a function call handler being broadcast into the pipeline only for the aggregator to log a warning and drop it.
LLMServicenow rejects results for a call already settled by a final result, a timeout, or a cancellation.
(PR #5291) -
Fixed the sequential function call runner (
run_in_parallel=False) shutting down when an in-flight call was cancelled, which left every later function call in the conversation unexecuted.
(PR #5291) -
Fixed
LiveKitTransportidentifying participants by LiveKit'ssid(a per-connection session id) everywhere it surfaces aparticipant_id— event handlers,LiveKitInputTransportMessageFrame,get_participants()— whileget_participant_metadata(),mute_participant(), andunmute_participant()look the id up inroom.remote_participants, which LiveKit keys byidentityinstead. The idget_participants()/events handed out could never be fed into those three lookup methods, soget_participant_metadata()silently returned{}andmute_participant()/unmute_participant()silently did nothing.participant_idis now consistently the participant's LiveKit identity throughout. Those three methods also referencedis_speakingandtracks, attributes the currentlivekitSDK no longer has (track_publicationsreplacestracks);get_participant_metadata()no longer includesis_speaking, and muting now unsubscribes from the participant's audio track viatrack_publications.
(PR #5297) -
Fixed
LiveKitTransportnever delivering client messages (including RTVI'sclient-readyhandshake) to the pipeline. Incoming data-channel messages were wrapped in an output-message frame and pushed downstream only, soRTVIProcessornever saw them — instead, the output transport picked the misrouted frame back up and echoed it straight back out to the room. Messages are now parsed and broadcast asInputTransportMessageFramein both directions, matching Daily and SmallWebRTC, so RTVI-based bots using LiveKit now complete the client-ready/bot-ready handshake and receive client messages correctly. Non-JSON or non-object data on the channel is ignored rather than raising, and still fireson_data_receivedfor backwards compatibility.
(PR #5297) -
Fixed xAI STT to use the pipeline input sample rate when no explicit rate is configured.
(PR #5298) -
Fixed AssemblyAI STT to use the pipeline input sample rate when no explicit rate is configured.
(PR #5298) -
Fixed Krisp VIVA support against SDK 1.11.0 and newer, which switched its Python bindings from pybind11 to nanobind.
KrispVivaFilter,KrispVivaTurn, andKrispVivaIPUserTurnStartStrategydetect the binding style at runtime, so both older and newer SDK builds work.KrispVivaFilter'snoise_suppression_levelis now a float; an int is still accepted.
(PR #5302) -
Fixed intermittent failures in tests written with
run_test(). Frames were sent after a fixed 10ms delay, so a pipeline that took longer than that to start would drop them;run_test()now waits for the pipeline to be ready before sending. A newstart_timeoutargument (1 second by default) raisesTimeoutErrorif the pipeline never starts.
(PR #5313) -
A service that fails to connect is left unable to do its job, so a
ServiceSwitchermoves off it before the pipeline starts and every frame reaches a service that connected. Setting up is not attempted again, so the failure is permanent whatever caused it: a connection timeout previously left the service usable and the switcher on it.
(PR #5316) -
A processor that raises while setting up now pushes an
ErrorFrameupstream, the same way a failure while handling a frame is reported, so application code learns its pipeline came up degraded. The error was previously only logged and the pipeline ran on regardless. Each failing processor reports its own error, so a pipeline where several fail reports all of them rather than only whichever raised first.
(PR #5316) -
DeepgramSTTServicestops reconnecting once three attempts in a row have failed to produce a connection that stays up, and reports itself unusable so aServiceSwitchermoves off it. A handshake that hung before failing, or a connection that dropped after a while, previously reset the count and it retried for the life of the process.
(PR #5316) -
A processor that raises while being cleaned up no longer costs the rest of the pipeline its teardown. Each failure is logged and every other processor is still released.
(PR #5316) -
Fixed TTFB being measured inconsistently across LLM services, so the values were not comparable between them. TTFB now represents the time to the first byte of the model's streamed response for every LLM service.
AnthropicLLMServiceandAWSBedrockLLMServicestopped measuring as soon as the stream was created, before reading any event, so their TTFB reflected connection setup rather than the model's response;GoogleLLMServicestopped on the first chunk, which can carry usage metadata and no model output. Reasoning is part of the response, so a thinking model's TTFB ends at its first reasoning token.TTFB values for these models may be increased.
(PR #5319) -
The eval judge waits for a bot that is still working instead of failing the turn. A bot with async tools acknowledges a request and answers once its tool returns, and the acknowledgement was being judged as a wrong answer: whether a reply counted as an answer turned on how it read, so a fluent "The system is checking the current conditions for you right now." was taken for one. A bot that says it is checking, fetching, or will report back has not answered yet, whatever the length or polish of the sentence it says it in.
(PR #5328) -
Fixed synthesis markup disappearing from a turn's spoken text when no word follows it. A tag closing a sentence — or sitting between the last word and its period — is never named by a word-timestamp event, so it was missing from
AggregatedTextProgressFrame.accumulated_textand from the text the turn reported as spoken.
(PR #5331) -
Fixed word-level TTS tracking stopping partway through a sentence containing synthesis markup, when the provider punctuates a tagged span differently from the source text.
(PR #5331) -
Fixed a sentence losing word-level TTS tracking when the synthesis markup comes from the LLM itself, e.g. an LLM prompted to emit
<spell>1234</spell>withSkipTagsAggregatorkeeping the tagged block intact. The whole sentence was treated as one untrackable unit: it reported no progress until it had finished speaking, and its words reached the conversation context only as a single block at the end. Now only the tagged span is committed whole, so every word around it gets its ownTTSTextFrameandAggregatedTextProgressFrame. Applies to bothSENTENCEandTOKENtext aggregation. Tags inserted by a text transform were never affected, since those reach the TTS without appearing in the user-facing text.
(PR #5331) -
Fixed VAD analyzers, turn analyzers, local audio and Tk output transports, and the Daily and Vonage clients leaking a worker thread per session, plus one per output destination of every transport. The thread pools they run blocking work on are now shut down at cleanup.
(PR #5350) -
Fixed
GoogleLLMServicebeing unusable withgemini-3.7-flash, which rejects theminimalthinking level Pipecat applies as a low-latency default (every request failed with400 INVALID_ARGUMENT). It now getslow, the lowest level it accepts.
(PR #5356) -
Fixed word-level TTS tracking breaking when a TTS service normalizes typographic punctuation in its word-timestamp events, e.g. reporting
don'tfor adon’tit was sent, or the reverse, and likewise for curly quotes and en/em dashes.
(PR #5357) -
Pinless dial-in update failures now trigger the Daily transport's
on_errorevent instead of only being logged.
(PR #5358) -
The
DeprecationWarningfor readingFrameProcessorSetup.tool_resourcesis reported once per call site, and points at the line that performed the read. A caller that reflects over every field of every object it sees — a frame serializer, for example — previously repeated the warning without bound.
(PR #5365) -
Fixed the Google LLM services ignoring the
seedsetting.GoogleLLMService,GoogleVertexLLMService,GeminiLiveLLMService, andGeminiLiveVertexLLMServicenow send it. Gemini treats a seed as best effort, so identical seeds usually but not always produce identical responses.
(PR #5366) -
Fixed
GoogleLLMServicenot applying its low-latency thinking defaults inrun_inference(). Now both in-pipeline andrun_inference()code paths build their request the same way. An explicitthinkingsetting still wins.
(PR #5368) -
Fixed word-level TTS tracking recording the TTS-side text in the conversation context instead of the LLM's original text when a TTS service's word-timestamp events don't spell a word the way it was sent. This fixes cases where a provider strips diacritics (e.g. recording cafe instead of the LLM's café) and where text is closed out early while carrying synthesis tags, causing those tags (e.g.
</spell>) to be recorded instead of the LLM's own pattern delimiters (e.g.</card>).
(PR #5370) -
Pipecat now requires
pydantic>=2.13on Python 3.14, where earlier pydantic releases ship no prebuilt wheels and must be compiled from source. Other Python versions are unaffected.
(PR #5375) -
Fixed a negative TTFB being reported by an STT service that finalizes a segment on its own endpointing and then returns nothing for the final segment. The timeout path measured to that earlier transcript, which predates the speech it measured from, reporting the service as responding before it was asked. Such an utterance now reports no TTFB, and the service is named in a warning.
(PR #5384) -
FrameProcessorMetrics.stop_ttfb_metrics()now refuses any measurement whose output predates its start, so a wall clock that steps backwards mid-measurement cannot put an impossible latency into the metrics stream. Processors can also callcancel_ttfb_metrics()to abandon a measurement whose response never arrived, rather than leaving it open for unrelated output to be measured against.
(PR #5384) -
Fixed
MuteUntilFirstBotCompleteUserMuteStrategyleaving the user muted for the rest of the call when the bot's first speaking turn failed. The strategy unmutes onBotStoppedSpeakingFrame, which a turn that produces no audio — a TTS failure, say — never emits, and a muted user can't prompt another turn to supply one. AnErrorFramearriving before the bot starts speaking now releases the mute as well; errors after that point are ignored, since the output transport ends the turn on its own once the audio dries up.
(PR #5390) -
Fixed
TavusTransportignoring itsbot_nameargument, so every Tavus bot joined the room named "Pipecat".
(PR #5392) -
Fixed
language_codebeing dropped oneleven_v3andeleven_v3_conversationalinElevenLabsHttpTTSService. The v3 models accept 74 languages — including Farsi, Pashto, and Sindhi, which no other ElevenLabs model covers — whereeleven_flash_v2_5andeleven_turbo_v2_5accept 32. A language the selected model doesn't support is dropped with a warning rather than sent.
(PR #5398) -
Fixed
LLMWorkerholding back frames that had nothing to do with a running tool. Frames queued while a@toolhandler ran were deferred until it finished, which was meant for the handler's own output but caught everything: frames arriving over the bus and the worker's own lifecycle frames were held too. Deferral now applies only to frames queued from inside a handler, or from something it awaits. An application event handler that queues a frame while a tool happens to be running is no longer held behind it.
(PR #5399) -
Fixed a worker acting on the same cancellation more than once. A worker receives more than one cancel on an ordinary shutdown, and only the pipeline frame was guarded, so each one was announced in the log again and propagated to every child, compounding down a tree of workers.
(PR #5399) -
Fixed the deprecation registry scanner crashing on editor lock files. A lock symlink left beside a source file, such as Emacs's
.#module.py, matched the scanner's glob but pointed at nothing readable, so pre-commit andtests/test_deprecation_markers.pyfailed for anyone with an unsaved buffer undersrc/.
(PR #5399) -
Fixed
PipelineWorker.flush_pipeline()reporting a drain that had not happened. The probe went straight to the pipeline, so it could overtake frames still waiting on the worker's push queue. It now queues behind them, while still bypassingqueue_frameoverrides such as the tool-call deferral.
(PR #5399) -
Fixed
FilterIncompleteUserTurnStrategiestalking over the user when a✓(complete) verdict resolved after the user had already resumed speaking. Such a completion is stale: the user turn stays open, so no new turn start — and no interruption — could cut the bot off.UserTurnCompletionLLMServiceMixinnow treats a✓that arrives while VAD hears the user as○, suppressing the response and re-arming the short re-prompt timeout.
(PR #5407) -
Fixed a
SIGSEGVinlibkrisp-audio-sdkon pipeline teardown:- The Krisp VIVA SDK is now initialized once per process, instead of being destroyed whenever the last
KrispVivaFilter,KrispVivaVadAnalyzer,KrispVivaTurn, orKrispVivaIPUserTurnStartStrategyreference was released. Its global state is shared by every Krisp session, so tearing it down while one of them was still processing audio left that session reading freed memory. KrispVivaVadAnalyzer.cleanup()is now idempotent, so the two calls the pipeline makes per session no longer release one reference more than it acquired.KrispVivaFilterandKrispVivaVadAnalyzerno longer release a reference their constructor never acquired.
(PR #5411)
- The Krisp VIVA SDK is now initialized once per process, instead of being destroyed whenever the last
-
Fixed a flat ~0.5s delay on every user turn with STT services that do their own end-of-turn detection (
CartesiaTurnsSTTService,DeepgramFluxSTTService,DeepgramFluxSageMakerSTTService, andAssemblyAISTTService/SonioxSTTServicewithvad_force_turn_endpoint=False).ProposedUserStoppedSpeakingFrameis now aControlFramerather than aSystemFrame, so it stays ordered behind the finalTranscriptionFramethese services push ahead of it.ExternalUserTurnStopStrategynow has the text it needs to close the turn as soon as the proposal arrives, instead of waiting out its aggregation timer.
(PR #5423) -
Fixed the WebSocket transports reporting a successful write for a frame that never went out, which pushed it downstream as though it had been delivered.
(PR #5424) -
Fixed
BaseOutputTransporthanging when a write to the transport never returns, for example when a client stops reading. The audio task stayed parked inside the write, so the bot went silent and theEndFramenever reached the end of the pipeline. Writes are now bounded by the newTransportParams.audio_out_write_timeout_secs(default 10s), and exceeding it leaves the transport unusable.
(PR #5424) -
Fixed
CerebrasLLMServicedroppingmax_tokens,frequency_penalty,presence_penaltyandservice_tierfrom chat completion requests. All four are supported by the Cerebras API and are now sent.
(PR #5448) -
Fixed the MoQ transport crashing when a fast peer's audio arrived before the input transport started: the session task died with
AttributeError: '_audio_in_queue'and the bot stayed silent for the rest of the call. Audio received beforeStartFramehas created the audio queue is now dropped.
(PR #5451)
Performance
-
Replaced the
pyloudnormdependency withloudness, which requires onlynumpy. This takesscipyout of the base install, where importing it accounted for roughly 690ms of cold start time.import pipecat.audio.utilsnow costs 0.25s instead of 0.95s.
(PR #5232) -
⚠️ LLMContextandLLMServiceno longer import the OpenAI SDK, so a pipeline that talks to another provider no longer loads it.LLMContexttakes its "not provided" sentinel from Pipecat rather than the SDK, andLLMService.adapter_classdefaults toNone, resolving toOpenAILLMAdapterat construction. Bots on a non-OpenAI provider save about 230ms of import.LLMService.adapter_classnow reads asNonerather thanOpenAILLMAdapterwhen a subclass doesn't set it; useget_llm_adapter()for the resolved adapter instance. Code comparingLLMContext'sNOT_GIVENagainst OpenAI's by identity should useis_given()instead.LLMContext(tools=...)andset_tools()likewise accept only Pipecat'sNOT_GIVENnow, so callers passing OpenAI's should pass Pipecat's or omit the argument.
(PR #5253) -
⚠️ Addedpipecat.utils.types, home of theNOT_GIVENsentinel now shared by settings,LLMContextand anything else needing "this value was not provided", together withis_given()andassert_given(). Provider SDKs keep their own equivalents, translated at the adapter boundary.NOT_GIVEN,NotGiven,is_given()andassert_given()now come frompipecat.utils.typesand are no longer importable frompipecat.services.settings. The privatepipecat.services.settings._NotGivenis now the publicpipecat.utils.types.NotGiven.The
is_given()exported by the OpenAI and Anthropic adapters tests that SDK's sentinel rather than Pipecat's, and is now namedopenai_is_given()andanthropic_is_given()to keep the two apart.pipecat.adapters.services.open_ai_adapteralso exports the translations that respell a context's values for the OpenAI SDK:openai_from_llm_context_tools(),openai_from_llm_context_tool_choice()andopenai_from_llm_standard_message(). The tools one is shared by the Chat Completions and Responses adapters.
(PR #5253) -
Cut Pipecat's import time roughly in half by loading heavy third-party dependencies on first use instead of at import. NLTK, which reaches
scikit-learnand in turnscipythrough its classifier backends, now loads insidematch_endofsentence(), andfastapiis type-checking-only inpipecat.runner.typesandpipecat.runner.utils. Withpyloudnormalready replaced,scipyno longer loads at all for a typical bot. Importing the modules a voice bot uses drops from about 2.2s to about 1.0s.PipelineWorkerwarms NLTK on a background thread as the pipeline starts, so the opening bot turn doesn't pay the load either. The NLTKpunkt_tabdata check, which can hit the network, moves off module import to that warming. Images that bundlepunkt_tabat build time, or setNLTK_DATAto a directory that has it, keep the warming off the network entirely.
(PR #5253) -
Bots start faster. Services and transports connect while the pipeline is setting up rather than when the
StartFramearrives, and a pipeline sets up and cleans up its processors concurrently, so startup costs the slowest service rather than the sum of them all.
(PR #5316)