# Differences from upstream

> Where pipedog behaves differently from Pipecat (Python), and why.

Source: https://polvallverdu.github.io/pipedog/docs/guides/differences-from-upstream/



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](https://polvallverdu.github.io/pipedog/docs/guides/coming-from-pipecat.md).

## Across the codebase [#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](https://polvallverdu.github.io/pipedog/docs/concepts/interruptions-and-cancellation.md).                                                                                                                                                           |
| 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](#llm-tools-and-direct-functions).                                                                                                                                                                                |
| 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 [#core]

### Utils [#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 [#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](https://polvallverdu.github.io/pipedog/docs/concepts/tts-audio-cache.md).
  `packages/core/src/services/tts_audio_cache.ts`

### Expressive conversation [#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 [#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 [#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](https://polvallverdu.github.io/pipedog/docs/concepts/metrics-and-tracing.md).
  `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 [#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 [#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 [#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 [#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 [#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](https://polvallverdu.github.io/pipedog/docs/concepts/turn-detection.md).
* **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 [#providers]

### Anthropic [#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 [#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 [#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 [#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 [#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 [#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 [#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](https://polvallverdu.github.io/pipedog/docs/guides/runtimes.md).
* **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 [#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 [#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 [#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 [#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 [#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 [#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 [#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](https://polvallverdu.github.io/pipedog/docs/flows/#differences-from-pipecat).
* **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](https://polvallverdu.github.io/pipedog/docs/evals/#differences-from-upstream).
* **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](https://polvallverdu.github.io/pipedog/docs/cli/#differences-from-pipecat).
* **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](https://polvallverdu.github.io/pipedog/docs/services/mcp-client/#differences-from-pipecat).
* **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](https://polvallverdu.github.io/pipedog/docs/transports/livekit.md) and
  [HeyGen](https://polvallverdu.github.io/pipedog/docs/services/video/heygen.md).
* **WhatsApp, local audio, Sentry, RNNoise and the soundfile mixer** each have a section:
  [WhatsApp](https://polvallverdu.github.io/pipedog/docs/transports/whatsapp/#differences-from-upstream),
  [local audio](https://polvallverdu.github.io/pipedog/docs/transports/local-audio/#differences-from-upstream),
  [Sentry metrics](https://polvallverdu.github.io/pipedog/docs/concepts/sentry-metrics/#differences-from-upstream),
  [RNNoise](https://polvallverdu.github.io/pipedog/docs/audio/rnnoise-filter/#differences-from-upstream) and
  [soundfile mixer](https://polvallverdu.github.io/pipedog/docs/audio/soundfile-mixer/#differences-from-upstream).
* **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](https://polvallverdu.github.io/pipedog/docs/audio/ducking.md).
* **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](https://polvallverdu.github.io/pipedog/docs/audio/ten-vad.md).
* **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](https://polvallverdu.github.io/pipedog/docs/services/tts/kokoro/#differences-from-upstream).
* **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](#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 [#reporting-a-difference]

If pipedog behaves differently from Pipecat and this page doesn't list it, please
[open an issue](https://github.com/polvallverdu/pipedog/issues) with the upstream behavior, what
pipedog does, and a small reproduction. See [Contributing](https://polvallverdu.github.io/pipedog/docs/contributing.md) to send a fix.
