pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog

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.

AreaUpstreampipedogWhy
ThreadsThreadPoolExecutor 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.
asyncioPython's asyncio.@pipedog/asyncio keeps asyncio's semantics, scheduling included.So ported code interleaves exactly as upstream. See Interruptions & cancellation.
Context variablescontextvars 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 clientsaiohttp sessions and httpx clients.The global fetch, or a fetch option.fetch exists on every runtime.
Provider SDKsThe 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.
Bytesbytes.Uint8Array, never Buffer.Buffer is a Node API. Core imports no node:* modules except node:async_hooks.
Filesystemaiofiles, pathlib.An injected reader or fs object (node:fs/promises satisfies it).Core never imports node:fs.
WebSocket handshakeThe 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 APIsKept 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).
PillowDecodes 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_SUPPORTSet to false at import by the Google services.Not set.There is no fork in JS.

Core

Utils

  • NOT_GIVEN is truthy. Upstream's sentinel is falsy, so value or default treats a missing value as absent. JavaScript objects are always truthy, so use isGiven(value) ? value : fallback. packages/core/src/utils/types.ts
  • Timeout exceptions. Upstream lists httpx TimeoutException. Here TIMEOUT_EXCEPTIONS holds the asyncio TimeoutError, and isTimeoutException also recognizes the DOMException named "TimeoutError" that a fetch aborted by AbortSignal.timeout() rejects with. packages/core/src/utils/http.ts
  • Connection limits. Upstream's connection_limits returns an httpx Limits instance. fetch has no such object, so connectionLimits returns 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). TTSService takes an optional audioCache, and warmAudioCache() 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 SpokenExpression count 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 \d also accepts other Unicode digits). packages/core/src/expressive/formatter.ts
  • ExpressionGuidance. Upstream's StrEnum is a class with static instances (value, values(), fromValue()) rather than an as const object, so instanceof can still tell a preset from a developer line with the same text, which upstream's isinstance check 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 every WARNING or 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 pipecat and pipecat.turn, and setup_tracing defaults service_name to "pipecat". Here they are pipedog, pipedog.turn and "pipedog", so traces from the two can be told apart. Span names and attribute keys are upstream's. setTracerName("pipecat") (or setupTracing({ 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 await suspends on a task. A Task is thenable so it can be awaited, but then() is also an ordinary method. Calling task.then(cb) yourself (Python has no equivalent) observes the task without making the caller wait on it: cancelling the caller doesn't cancel task. await, Promise.all and Promise.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/asyncio has 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.on when 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 or gc.garbage. The runner calls the global gc() 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.capabilities with a plain dict. TypeScript does not allow an override of a different type, so the backend's capabilities are backendCapabilities, and PipelineWorker.capabilities keeps 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 DirectFunctionMetadata attached 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. _collectTools returns the methods found on the prototype chain, not bound methods, because a bound function does not expose the source its schema is read from. LLMWorker binds 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_CALL is a context variable. Here the worker running a tool is recorded per task, so a task the tool handler spawns (with gather or createTask) 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.ts See 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: false for a session of its own. packages/audio-silero/src/silero.ts
  • minVolumeDb (pipedog only). VADParams takes the volume gate as a loudness in LUFS as an alternative to the 0–1 minVolume, 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). MinWordsUserTurnStartStrategy takes isBackchannel (with createBackchannelMatcher() and per-language BACKCHANNEL_PHRASES), so an acknowledgement over the bot never starts a turn, and requireVadWithinSeconds, 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.FIRST list. Upstream's WakePhraseUserTurnStartStrategy, while asleep, still keeps the start strategies after it in a FIRST list from seeing frames, through the deprecated ProcessFrameResult.STOP. pipedog doesn't port ProcessFrameResult, so every strategy sees every frame: use the wake phrase strategy alone, or combine it with other conditions in a StrategyMode.ALL list (both work the same upstream). packages/core/src/turns/user_start/wake_phrase_user_turn_start_strategy.ts

Providers

Anthropic

  • Explicit null sampling values are omitted. The Python SDK's NOT_GIVEN has no JS counterpart, so temperature, topK, topP and thinking store null for "not sent". An explicit null is left out of the request, where upstream would send JSON null. 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 a credentialsProvider function 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 fetch or 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-runtime on Node and Bun. packages/aws/src/nova_sonic/llm.ts
  • SageMaker streaming needs HTTP/2. SageMakerBidiClient loads the optional @aws-sdk/client-sagemaker-runtime-http2 peer, 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_config is not ported: no client-side retries of throttled or 5xx requests. Pass a fetch to 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 an ErrorFrame. packages/aws/src/tts.ts

Azure

  • No Speech SDK. Upstream uses azure-cognitiveservices-speech, a native library. Azure Speech STT and AzureTTSService speak the Speech service's WebSocket protocol directly (the one the SDKs use), and AzureHttpTTSService calls the REST endpoint with fetch, so all of them run on Bun, Node, Deno and Workers. _speechConfig is a small stand-in for the SDK's SpeechConfig. 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
  • fetch replaces aiohttp_session in AzureImageGenServiceREST, with an injectable image decoder instead of Pillow. packages/azure/src/image.ts

Cartesia

  • fetch replaces aiohttp_session. CartesiaHttpTTSService posts with the global fetch, or the fetch option. packages/cartesia/src/tts.ts
  • No handshake headers. The standard WebSocket does not expose the handshake response, so the X-Request-ID header upstream logs on connect is always reported as None. packages/cartesia/src/stt.ts

Deepgram

  • fetch replaces aiohttp_session. DeepgramHttpTTSService takes an optional fetch where upstream requires a shared aiohttp_session. packages/deepgram/src/tts.ts
  • No Python SDK. STT uses a minimal DeepgramClient that speaks the same /v1/listen WebSocket protocol. DeepgramApiError is 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 the dg-* response headers upstream logs on connect. Query values use JS number formatting, so speed=1.0 is written speed=1. packages/deepgram/src/tts.ts

ElevenLabs

  • fetch replaces aiohttp_session. ElevenLabsHttpTTSService and ElevenLabsSTTService take an optional fetch where upstream requires an aiohttp_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 WebSocket has no message size limit to set. packages/elevenlabs/src/tts_base.ts

Google

  • 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 credentials are 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 from location and 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 imageDecoder option; 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). GeminiSTTService takes vertexai: true with location, projectId, and credentials / credentialsPath (or ADC) instead of apiKey, as the Vertex LLM services do. Vertex's Live API rejects languageAuto and languageHints, so in that mode the codes go in inputAudioTranscription.languageCodes and 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: false puts the configured language (when exactly one is set) or null on transcription frames instead of the language the model reports. setLanguageCodes(codes | () => codes) changes the languages during a session; the service reconnects on the next BotStartedSpeakingFrame, 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 the StartFrame waits for it. Here it is opened in a background task on StartFrame and audio is buffered until the session is live; a failure is reported through on_connection_error. awaitConnectionOnStart: true (pipedog-only) restores the blocking behaviour. packages/google/src/gemini_live/stt.ts
  • retryOnBlocked on GeminiTTSService (pipedog-only). Gemini TTS sometimes refuses a harmless sentence for Google's usage guidelines, and the same request usually succeeds on the next try. With retryOnBlocked the sentence is requested again (retries, default 1); if part of its audio already played, it starts over (optionally as restartText(text), e.g. "Sorry, …") rather than resuming mid-sentence. A fallback, a GeminiTTSSettings delta 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 an ErrorFrame as upstream. packages/google/src/tts.ts
  • Shared Cloud clients and prewarm() (pipedog-only). sharedClient: true on GoogleSTTService, GoogleTTSService, GoogleHttpTTSService and GeminiTTSService (Cloud backend) uses one gRPC client per process for the same endpoint, credentials and client options (getSharedGoogleCloudTTSClient for TTS). prewarm() loads the SDK, opens the channel and fetches a token at start-up, and returns false instead 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 the languageCodes mid-call (an array, or a function re-read on each BotStartedSpeakingFrame). The stream reconnects on the next BotStartedSpeakingFrame, or after the user's turn if they are speaking over the bot, so no utterance is cut. Same API as GeminiSTTService. 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's voiceCloningKey (hashed) and the backend of GeminiTTSService. 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
  • fetch replaces aiohttp_session in 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 content is a list, upstream stringifies it with Python's str(). pipedog uses JSON. packages/langchain/src/langchain.ts

OpenAI

  • fetch replaces aiohttp_session. OpenAIImageGenService downloads hosted results with the global fetch, or the fetch option. packages/openai/src/image.ts
  • decodeImage (pipedog-only). Upstream decodes with Pillow. PNGs are decoded with fast-png by default, and decodeImage can decode other formats. packages/openai/src/image.ts
  • httpClient is a fetch. Upstream takes an httpx client. Here httpClient is the SDK's fetch option, 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 through serve() and mounts in any framework. Startup hooks are lifespan functions that may return a cleanup. packages/runner/src/run.ts
  • The prebuilt client ships in the package. Upstream installs it from the pipecat-ai-prebuilt wheel; @pipedog/runner bundles the same built client. packages/runner/src/prebuilt.ts
  • Unported transports fail clearly. Daily (with --direct and 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's eval-connect message 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 start message is a TelephonyCallEndedError (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 websockets package and Starlette's WebSocket. pipedog's transports sit on a runtime-agnostic WebsocketConnection. The FastAPI transport takes any socket your server accepted, on any runtime. Workers can't listen on a port, so use FastAPIWebsocketTransport with a WebSocketPair there. 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 a readFile function you pass in. See Flows.
  • Evals are the pipedog-eval bin, factory: entries are imported with import(), each suite run is a child process, and the deprecated EvalTransport server and harness are 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/list cursor, where upstream reads the first page, and on Workers does not validate a result against the tool's output schema (the validator needs new Function). See MCP client.
  • LiveKit and HeyGen take a fetch option in place of upstream's aiohttp session, 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 fetch option in place of the aiohttp session or the provider's SDK client, a direct WebSocket client where upstream uses a Python SDK (Sarvam, Camb.ai), and no deprecated InputParams. Where a service sends floats as JSON or MessagePack, a whole number such as speed: 1.0 is written 1, 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).

On this page