Differences from upstream
Where pipedog behaves differently from Pipecat (Python), and why.
pipedog aims for 1:1 behavior with Pipecat: the same frames in the same order, the same lifecycle
and the same wire formats. Where JavaScript or the runtimes force a difference, the code marks it
with a DIVERGENCE comment. This page collects them.
It covers behavior. For how names and APIs map (camelCase, options objects, enums and so on), see Coming from Pipecat.
Across the codebase
These differences apply everywhere, not to one module. They come from the porting rules in
docs/CONVENTIONS.md and the "Porting notes" at the top of the provider source files.
| Area | Upstream | pipedog | Why |
|---|---|---|---|
| Threads | ThreadPoolExecutor and run_in_executor run blocking work in a thread. | The work runs inline (async) or in an injected runner. | There are no threads on Cloudflare Workers. Keep per-call CPU low there. |
| asyncio | Python's asyncio. | @pipedog/asyncio keeps asyncio's semantics, scheduling included. | So ported code interleaves exactly as upstream. See Interruptions & cancellation. |
| Context variables | contextvars carry state into tasks. | Core has none (no node:async_hooks outside @pipedog/asyncio). State is recorded per task instead. | Core stays runtime-agnostic. One visible effect is listed under LLM tools. |
| HTTP clients | aiohttp sessions and httpx clients. | The global fetch, or a fetch option. | fetch exists on every runtime. |
| Provider SDKs | The provider's Python SDK. | The provider's JS SDK when it works over fetch or WebSocket (for example openai, @google/genai, @langchain/core). Otherwise a small client over fetch or the global WebSocket. Node SDKs only where the provider offers nothing but gRPC streaming. | So each service runs on as many runtimes as the provider allows. |
| Bytes | bytes. | Uint8Array, never Buffer. | Buffer is a Node API. Core imports no node:* modules except node:async_hooks. |
| Filesystem | aiofiles, pathlib. | An injected reader or fs object (node:fs/promises satisfies it). | Core never imports node:fs. |
| WebSocket handshake | The websockets package exposes the handshake response. | A standard WebSocket does not, so response headers and the HTTP status of a refused handshake are not available. | The standard API has no access to them. Affected services are listed below. |
| Deprecated APIs | Kept until removal, with warnings. | Not ported: deprecated classes, aliases, parameters and fields are left out. | pipedog starts without the deprecated surface. Each module's dropped items are in its notes in porting/manifest.json. Moved modules exist only at their new path: the LLM adapters are under adapters/llm (pipecat 1.14 keeps the old pipecat.adapters.* paths as deprecated shims). |
| Pillow | Decodes and encodes images. | Built-in or injectable decoders and encoders (for example utils/jpeg_encoder.ts, an imageDecoder option). | No native dependencies. |
GRPC_ENABLE_FORK_SUPPORT | Set to false at import by the Google services. | Not set. | There is no fork in JS. |
Core
Utils
NOT_GIVENis truthy. Upstream's sentinel is falsy, sovalue or defaulttreats a missing value as absent. JavaScript objects are always truthy, so useisGiven(value) ? value : fallback.packages/core/src/utils/types.ts- Timeout exceptions. Upstream lists httpx
TimeoutException. HereTIMEOUT_EXCEPTIONSholds the asyncioTimeoutError, andisTimeoutExceptionalso recognizes theDOMExceptionnamed "TimeoutError" that afetchaborted byAbortSignal.timeout()rejects with.packages/core/src/utils/http.ts - Connection limits. Upstream's
connection_limitsreturns an httpxLimitsinstance.fetchhas no such object, soconnectionLimitsreturns the plain options for an SDK or agent that reads them field by field.packages/core/src/utils/http.ts
TTS
- Audio cache (pipedog only).
TTSServicetakes an optionalaudioCache, andwarmAudioCache()synthesizes fixed lines ahead of time, so they play without a request to the provider. Off by default. See TTS audio cache.packages/core/src/services/tts_audio_cache.ts
Expressive conversation
- Text offsets. The expression formatter and
SpokenExpressioncount offsets in UTF-16 code units (JavaScript string indices), not code points, so they differ from upstream only after an astral character such as an emoji. A[[pause N]]tag takes ASCII digits only (Python's\dalso accepts other Unicode digits).packages/core/src/expressive/formatter.ts ExpressionGuidance. Upstream'sStrEnumis a class with static instances (value,values(),fromValue()) rather than anas constobject, soinstanceofcan still tell a preset from a developer line with the same text, which upstream'sisinstancecheck relies on.packages/core/src/expressive/policy.ts
Logging
- Secrets are masked in warnings and errors. Upstream masks credentials only in
ErrorFrame. pipedog masks the message and attached error of everyWARNINGor higher log record, for every sink, because these often quote what failed, such as a URL with?key=....packages/core/src/utils/logger.ts
Tracing
- Tracer names and the default service name say pipedog. Upstream's tracers are
pipecatandpipecat.turn, andsetup_tracingdefaultsservice_nameto"pipecat". Here they arepipedog,pipedog.turnand"pipedog", so traces from the two can be told apart. Span names and attribute keys are upstream's.setTracerName("pipecat")(orsetupTracing({ tracerName: "pipecat" })) brings the upstream tracer names back, for dashboards and queries that filter on them: see Metrics and tracing.packages/core/src/utils/tracing/setup.ts forceFlushTracing()waits for exports already in flight. The SDK's batch processor only flushes the spans still queued, so a flush that landed just after its 5 s timer started an export returned before the collector had the spans.packages/tracing/src/index.ts
asyncio
- Only
awaitsuspends on a task. ATaskis thenable so it can be awaited, butthen()is also an ordinary method. Callingtask.then(cb)yourself (Python has no equivalent) observes the task without making the caller wait on it: cancelling the caller doesn't canceltask.await,Promise.allandPromise.resolve(task)still suspend the caller, as in asyncio.packages/asyncio/src/task.ts
asyncio on Cloudflare Workers
- Callbacks run in the I/O context that scheduled them. One workerd isolate serves many
requests and Durable Objects, and I/O must happen in the context that owns it. On Workers,
@pipedog/asynciohas no shared ready queue: each callback runs in the context that scheduled it, still in FIFO order. That is what lets several Durable Objects run voice sessions in one isolate at once. Bun, Node and Deno keep a single ready queue, like CPython.packages/asyncio/src/loop.ts
Workers and runner
- Signal handlers. SIGINT and SIGTERM handlers are installed through the host's
process.onwhen there is one (Bun, Node, Deno). On runtimes without process signals (Workers) they are a no-op.packages/core/src/workers/runner.ts - Garbage collection. JS has no
gc.collect()count orgc.garbage. The runner calls the globalgc()when the runtime exposes one (for example Node or Deno with--expose-gc), and skips it otherwise.packages/core/src/workers/runner.ts - Backend capabilities. Upstream overrides
PipelineWorker.capabilitieswith a plain dict. TypeScript does not allow an override of a different type, so the backend's capabilities arebackendCapabilities, andPipelineWorker.capabilitieskeeps its meaning.packages/core/src/workers/llm/backend_llm_worker.ts
LLM tools and direct functions
- Direct function schemas. JavaScript has no runtime type hints or docstrings. Parameter names,
and which ones are required (no default value), are read from the function's source. The
description and parameter types come from optional
DirectFunctionMetadataattached to the function. Parameters without metadata get an any-type schema. Minified functions lose their parameter names, so direct functions must ship unminified.packages/core/src/adapters/llm/schemas/direct_function.ts - Collected tools are unbound.
_collectToolsreturns the methods found on the prototype chain, not bound methods, because a bound function does not expose the source its schema is read from.LLMWorkerbinds each one when it registers it.packages/core/src/workers/llm/tool_decorator.ts - Tool-call state does not reach spawned tasks. Upstream's
_IN_TOOL_CALLis a context variable. Here the worker running a tool is recorded per task, so a task the tool handler spawns (withgatherorcreateTask) does not inherit it, and frames queued from such a task are not held.packages/core/src/workers/llm/llm_worker.ts
Turn analyzers and VAD
- Smart Turn loads lazily. Upstream loads the model when the analyzer is created. pipedog
loads it on the first prediction, so an unused analyzer loads nothing. Without the inference
runtime package (or where it can't load), turns end on the VAD's silence instead of failing. A
factory passed in explicitly still fails loudly.
packages/core/src/audio/turn/smart_turn/local_smart_turn_v3.tsSee Turn detection. - Silero sessions are shared. Upstream loads one session per model. pipedog shares sessions by
default, since a bot builds one VAD per call. Pass
shareSession: falsefor a session of its own.packages/audio-silero/src/silero.ts minVolumeDb(pipedog only).VADParamstakes the volume gate as a loudness in LUFS as an alternative to the 0–1minVolume, converted with the same mapping (volumeFromDb,volumeToDb). Setting both throws.packages/core/src/audio/vad/vad_analyzer.ts- Backchannels and VAD-backed words (pipedog only).
MinWordsUserTurnStartStrategytakesisBackchannel(withcreateBackchannelMatcher()and per-languageBACKCHANNEL_PHRASES), so an acknowledgement over the bot never starts a turn, andrequireVadWithinSeconds, so words over the bot count only if the VAD heard the user recently. Both are off by default.packages/core/src/turns/user_start/min_words_user_turn_start_strategy.ts,packages/core/src/turns/user_start/backchannel.ts - A wake phrase doesn't gate a
StrategyMode.FIRSTlist. Upstream'sWakePhraseUserTurnStartStrategy, while asleep, still keeps the start strategies after it in aFIRSTlist from seeing frames, through the deprecatedProcessFrameResult.STOP. pipedog doesn't portProcessFrameResult, so every strategy sees every frame: use the wake phrase strategy alone, or combine it with other conditions in aStrategyMode.ALLlist (both work the same upstream).packages/core/src/turns/user_start/wake_phrase_user_turn_start_strategy.ts
Providers
Anthropic
- Explicit
nullsampling values are omitted. The Python SDK'sNOT_GIVENhas no JS counterpart, sotemperature,topK,topPandthinkingstorenullfor "not sent". An explicitnullis left out of the request, where upstream would send JSONnull.packages/anthropic/src/llm.ts
AWS
- No botocore credential chain. Keys come from the options or the
AWS_*environment variables, as upstream, and beyond that from acredentialsProviderfunction you pass (called per request or per session, so it can renew temporary credentials). Instance profiles, IRSA, ECS roles, SSO and credential files are not read for you.packages/aws/src/utils.ts - Nova Sonic over WebSocket. Upstream streams over HTTP/2 with the experimental Python SDK.
Full-duplex HTTP/2 isn't available to
fetchor on Workers, so the default client speaks the same API over Bedrock's WebSocket endpoint (the transport the AWS JS SDK uses in browsers), on every runtime.transport: "http2"uses@aws-sdk/client-bedrock-runtimeon Node and Bun.packages/aws/src/nova_sonic/llm.ts - SageMaker streaming needs HTTP/2.
SageMakerBidiClientloads the optional@aws-sdk/client-sagemaker-runtime-http2peer, which runs on Node 22+ and Bun, not on Deno or Workers. Its events use the JS SDK's shapes.packages/aws/src/sagemaker/bidi_client.ts - No botocore retries. Bedrock's
client_configis not ported: no client-side retries of throttled or 5xx requests. Pass afetchto add them.packages/aws/src/llm.ts - Every Polly failure is an
ErrorFrame. Upstream catches botocore errors only; here a non-2xx response, a network error or missing credentials all become anErrorFrame.packages/aws/src/tts.ts
Azure
- No Speech SDK. Upstream uses
azure-cognitiveservices-speech, a native library. Azure Speech STT andAzureTTSServicespeak the Speech service's WebSocket protocol directly (the one the SDKs use), andAzureHttpTTSServicecalls the REST endpoint withfetch, so all of them run on Bun, Node, Deno and Workers._speechConfigis a small stand-in for the SDK'sSpeechConfig.packages/azure/src/stt.ts,packages/azure/src/tts.ts - STT connects in
start(). The connection opens once the input sample rate is known, and the next audio reopens it after the service closes it.packages/azure/src/stt.ts - Interruptions close the TTS connection. Upstream calls the SDK's
stop_speaking_async(); here the synthesis connection is closed and reopens on the next synthesis.packages/azure/src/tts.ts fetchreplacesaiohttp_sessioninAzureImageGenServiceREST, with an injectable image decoder instead of Pillow.packages/azure/src/image.ts
Cartesia
fetchreplacesaiohttp_session.CartesiaHttpTTSServiceposts with the globalfetch, or thefetchoption.packages/cartesia/src/tts.ts- No handshake headers. The standard
WebSocketdoes not expose the handshake response, so theX-Request-IDheader upstream logs on connect is always reported asNone.packages/cartesia/src/stt.ts
Deepgram
fetchreplacesaiohttp_session.DeepgramHttpTTSServicetakes an optionalfetchwhere upstream requires a sharedaiohttp_session.packages/deepgram/src/tts.ts- No Python SDK. STT uses a minimal
DeepgramClientthat speaks the same/v1/listenWebSocket protocol.DeepgramApiErroris raised only when the runtime's error carries the handshake status; otherwise a refused handshake is retried until the quick-failure tracker gives up.packages/deepgram/src/stt.ts - No
dg-*headers. The TTS service cannot log thedg-*response headers upstream logs on connect. Query values use JS number formatting, sospeed=1.0is writtenspeed=1.packages/deepgram/src/tts.ts
ElevenLabs
fetchreplacesaiohttp_session.ElevenLabsHttpTTSServiceandElevenLabsSTTServicetake an optionalfetchwhere upstream requires anaiohttp_session.packages/elevenlabs/src/tts.ts,packages/elevenlabs/src/stt.ts- No message size setting. Upstream sets a 16 MB max WebSocket message size for large audio
responses. The standard
WebSockethas no message size limit to set.packages/elevenlabs/src/tts_base.ts
- Credentials resolve later. Upstream resolves credentials (and, for STT, the project ID) in
the constructor. Reading a key file, finding Application Default Credentials and refreshing a
token are asynchronous in JS, so they happen on first connect or first request. A missing
credential surfaces then (as "No valid credentials provided.", or for Cloud TTS as a synthesis
error). Inline
credentialsare still checked in the constructor.packages/google/src/stt.ts,packages/google/src/vertex/llm.ts,packages/google/src/gemini_live/vertex/llm.ts,packages/google/src/tts.ts clientOptions(pipedog-only). The Cloud STT and TTS services accept extra client options, merged over the ones derived fromlocationand the credentials. Upstream builds its client from those alone.packages/google/src/stt.ts,packages/google/src/tts.ts- gRPC services need a server runtime. Cloud Speech streaming and Cloud TTS go through the
Node SDKs (
@google-cloud/speech,@google-cloud/text-to-speech), loaded lazily; they run on Bun, Node and Deno, not Workers. Vertex services authenticate through google-auth-library and run on Bun, Node and Deno, not Workers. See Runtimes. - Image decoding is injectable. Upstream decodes inline images with Pillow. Here that is the
imageDecoderoption; the default throws "not ported yet", so without one an image response surfaces as an error.packages/google/src/llm.ts,packages/google/src/image.ts - Gemini STT on Vertex AI (pipedog-only).
GeminiSTTServicetakesvertexai: truewithlocation,projectId, andcredentials/credentialsPath(or ADC) instead ofapiKey, as the Vertex LLM services do. Vertex's Live API rejectslanguageAutoandlanguageHints, so in that mode the codes go ininputAudioTranscription.languageCodesand none means automatic detection. The client is created on first connect rather than in the constructor, so a subclass's_createClient()can read its own fields.packages/google/src/gemini_live/stt.ts - Gemini STT language options (pipedog-only).
reportDetectedLanguage: falseputs the configured language (when exactly one is set) ornullon transcription frames instead of the language the model reports.setLanguageCodes(codes | () => codes)changes the languages during a session; the service reconnects on the nextBotStartedSpeakingFrame, or after the user's turn if they are speaking, buffering audio meanwhile.packages/google/src/gemini_live/stt.ts - Gemini STT connects in the background. Upstream opens the Live session in
setup(), so theStartFramewaits for it. Here it is opened in a background task onStartFrameand audio is buffered until the session is live; a failure is reported throughon_connection_error.awaitConnectionOnStart: true(pipedog-only) restores the blocking behaviour.packages/google/src/gemini_live/stt.ts retryOnBlockedonGeminiTTSService(pipedog-only). Gemini TTS sometimes refuses a harmless sentence for Google's usage guidelines, and the same request usually succeeds on the next try. WithretryOnBlockedthe sentence is requested again (retries, default 1); if part of its audio already played, it starts over (optionally asrestartText(text), e.g. "Sorry, …") rather than resuming mid-sentence. Afallback, aGeminiTTSSettingsdelta or a function yielding frames, gets one last attempt. Only the usage-guidelines block is retried (isGeminiTTSBlockedError), and restarted or fallback audio is never cached. Off by default, so a blocked sentence fails with anErrorFrameas upstream.packages/google/src/tts.ts- Shared Cloud clients and
prewarm()(pipedog-only).sharedClient: trueonGoogleSTTService,GoogleTTSService,GoogleHttpTTSServiceandGeminiTTSService(Cloud backend) uses one gRPC client per process for the same endpoint, credentials and client options (getSharedGoogleCloudTTSClientfor TTS).prewarm()loads the SDK, opens the channel and fetches a token at start-up, and returnsfalseinstead of throwing. Opt-in, because one shared channel carries every concurrent stream. Upstream builds a client per service.packages/google/src/tts.ts,packages/google/src/stt.ts GoogleSTTService.setLanguageCodes()(pipedog-only). Changes thelanguageCodesmid-call (an array, or a function re-read on eachBotStartedSpeakingFrame). The stream reconnects on the nextBotStartedSpeakingFrame, or after the user's turn if they are speaking over the bot, so no utterance is cut. Same API asGeminiSTTService.packages/google/src/stt.ts- Audio cache key for Google TTS. With the pipedog-only TTS audio cache, the key also covers
what changes the audio outside
settings:GoogleTTSService'svoiceCloningKey(hashed) and the backend ofGeminiTTSService.packages/google/src/tts.ts
Inworld and xAI realtime
- Status details are JSON. When a realtime response fails, upstream reports
str(status_details), a Python dict repr. pipedog renders it as JSON.packages/inworld/src/realtime/llm.ts,packages/xai/src/realtime/llm.ts fetchreplacesaiohttp_sessionin the Inworld and xAI HTTP TTS services.packages/inworld/src/tts.ts,packages/xai/src/tts.ts
LangChain
- List content is JSON. When a chunk's
contentis a list, upstream stringifies it with Python'sstr(). pipedog uses JSON.packages/langchain/src/langchain.ts
OpenAI
fetchreplacesaiohttp_session.OpenAIImageGenServicedownloads hosted results with the globalfetch, or thefetchoption.packages/openai/src/image.tsdecodeImage(pipedog-only). Upstream decodes with Pillow. PNGs are decoded withfast-pngby default, anddecodeImagecan decode other formats.packages/openai/src/image.tshttpClientis afetch. Upstream takes an httpx client. HerehttpClientis the SDK'sfetchoption, so httpx connection limits do not carry over.packages/openai/src/tts.ts,packages/whisper/src/base_stt.ts- Tool message content is JSON. In the Responses adapter, non-string tool content is written
as JSON for objects, where upstream uses Python's
str()(a Python repr).packages/openai/src/open_ai_responses_adapter.ts
Runner
- A fetch handler instead of FastAPI. The runner's server is a fetch handler (
app.fetch), so it runs on Bun, Node and Deno throughserve()and mounts in any framework. Startup hooks arelifespanfunctions that may return a cleanup.packages/runner/src/run.ts - The prebuilt client ships in the package. Upstream installs it from the
pipecat-ai-prebuiltwheel;@pipedog/runnerbundles the same built client.packages/runner/src/prebuilt.ts - Unported transports fail clearly. Daily (with
--directand the dial-in webhook), MOQ and the Vonage Video Connector are recognized, and fail with a message naming them. WhatsApp (--whatsapp) is ported. An eval harness'seval-connectmessage is not detected.packages/runner/src/run.ts,packages/runner/src/utils.ts - Hang-ups during the handshake are typed. A caller who hangs up before the provider's
startmessage is aTelephonyCallEndedError(a subclass of upstream's error), logged at info level.packages/runner/src/utils.ts - Async helpers. The WebSocket token helpers are async, because WebCrypto's HMAC is.
packages/runner/src/run.ts
Telephony serializers
The Twilio, Telnyx, Plivo, Exotel, Vonage and Genesys serializers keep upstream's wire formats and
behavior. Hang-ups through the provider's REST API use fetch instead of aiohttp, so they work on every
runtime, Workers included.
Transports
- SmallWebRTC completes pending writes on stop. Upstream leaves queued writes pending when a
track stops, so a write in progress when the peer connection is replaced (
restart_pc) waits for the output transport's write timeout and the pipeline fails. pipedog completes them.packages/transport-smallwebrtc/src/transport.ts - SmallWebRTC uses werift. Upstream is written against aiortc. pipedog wraps werift, a pure
TypeScript WebRTC stack, in the subset of aiortc's API upstream uses.
packages/transport-smallwebrtc/src/rtc.ts - WebSocket transports bring their own socket. Upstream sits on the
websocketspackage and Starlette'sWebSocket. pipedog's transports sit on a runtime-agnosticWebsocketConnection. The FastAPI transport takes any socket your server accepted, on any runtime. Workers can't listen on a port, so useFastAPIWebsocketTransportwith aWebSocketPairthere. Fetch-handler helpers (websocketFetchHandler,upgradeWebSocket,serveFetch) accept the socket on each runtime: from the request on Deno and Workers, with the server on Bun and Node.packages/transport-websocket/src/socket.ts,packages/transport-websocket/src/fastapi.ts
Other packages
Most newer packages list their own differences next to their usage, on the page named below. The main ones, in short:
- Flows behave as upstream; the API is camelCase, a tool returns
[result, nextNode], and files are read through areadFilefunction you pass in. See Flows. - Evals are the
pipedog-evalbin,factory:entries are imported withimport(), each suite run is a child process, and the deprecatedEvalTransportserver andharnessare left out. See Evals. - The CLI is a TypeScript project scaffold: no Pipecat Cloud, no Daily, no plugin system, and the docs MCP server in place of the Context Hub. See CLI.
- The MCP client follows a server's
tools/listcursor, where upstream reads the first page, and on Workers does not validate a result against the tool's output schema (the validator needsnew Function). See MCP client. - LiveKit and HeyGen take a
fetchoption in place of upstream'saiohttpsession, and the token helpers are async (WebCrypto signs the JWT). HeyGen's LiveAvatar fields that its API reference documents as optional (livekitAgentToken,wsUrl) are optional here, where upstream's model rejects a session without them. See LiveKit and HeyGen. - WhatsApp, local audio, Sentry, RNNoise and the soundfile mixer each have a section: WhatsApp, local audio, Sentry metrics, RNNoise and soundfile mixer.
- Ducking (
@pipedog/audio-ducking) is pipedog only: an output audio filter that lowers the bot's voice while the user talks over it. See Ducking. - TEN VAD (
@pipedog/audio-ten-vad) is pipedog only:TenVADAnalyzer, an opt-in alternative to Silero that drives TEN VAD's WebAssembly build. pipedog ships none of TEN's files; you obtain them from TEN, under TEN's license, and pass them in. See TEN VAD. - Local speech models (Whisper, Moonshine, FunASR, Kokoro, Piper, Pocket TTS) run on ONNX Runtime instead of the Python libraries. Each page has a Differences section, for example Kokoro.
- The speech providers (AssemblyAI, Speechmatics, Gladia, Soniox, Rime, Fish, Hume, Sarvam,
Smallest, Gradium, Meta, Neuphonic, Speechify, MiniMax, Resemble AI, Camb.ai, Async, Bland) follow the
rules in Across the codebase: a
fetchoption in place of theaiohttpsession or the provider's SDK client, a direct WebSocket client where upstream uses a Python SDK (Sarvam, Camb.ai), and no deprecatedInputParams. Where a service sends floats as JSON or MessagePack, a whole number such asspeed: 1.0is written1, because JavaScript has one number type. Each page's notes list what it leaves out.
Reporting a difference
If pipedog behaves differently from Pipecat and this page doesn't list it, please open an issue with the upstream behavior, what pipedog does, and a small reproduction. See Contributing to send a fix.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).