<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rehan Sanjay Venkatesan</title>
    <description>The latest articles on DEV Community by Rehan Sanjay Venkatesan (@rehansanjay).</description>
    <link>https://dev.to/rehansanjay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4183057%2F53a6aa37-494f-4e08-9c5c-f19d188d79cf.png</url>
      <title>DEV Community: Rehan Sanjay Venkatesan</title>
      <link>https://dev.to/rehansanjay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rehansanjay"/>
    <language>en</language>
    <item>
      <title>I measured what jambonz actually sends on a websocket, instead of reading the docs</title>
      <dc:creator>Rehan Sanjay Venkatesan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:42:10 +0000</pubDate>
      <link>https://dev.to/rehansanjay/i-measured-what-jambonz-actually-sends-on-a-websocket-instead-of-reading-the-docs-phb</link>
      <guid>https://dev.to/rehansanjay/i-measured-what-jambonz-actually-sends-on-a-websocket-instead-of-reading-the-docs-phb</guid>
      <description>&lt;p&gt;Pipecat ships transport serializers for Twilio, Telnyx, Plivo, Exotel, Vonage and Genesys. A developer choosing a phone layer has six options, and &lt;a href="https://jambonz.org" rel="noopener noreferrer"&gt;jambonz&lt;/a&gt; — open source, self-hostable, already capable of forking call audio to a websocket — isn't one of them.&lt;/p&gt;

&lt;p&gt;So I wrote it. This post is about the part before the code: finding out what the socket actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I couldn't look up
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;FrameSerializer&lt;/code&gt; in Pipecat is two methods. &lt;code&gt;serialize&lt;/code&gt; turns a Pipecat frame into something to write to the socket; &lt;code&gt;deserialize&lt;/code&gt; turns a socket message into a frame. To write either I needed to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What sample rate does jambonz actually send?&lt;/strong&gt; The &lt;code&gt;listen&lt;/code&gt; verb takes a &lt;code&gt;sampleRate&lt;/code&gt;. Does it honour it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the framing stable?&lt;/strong&gt; That decides whether &lt;code&gt;deserialize&lt;/code&gt; needs a variable-length path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does control data share the socket with audio?&lt;/strong&gt; This is the one that changes the shape of the class. Exotel base64s audio into JSON events, so everything is one stream of JSON. If jambonz interleaves JSON control frames with raw binary audio, &lt;code&gt;deserialize&lt;/code&gt; has to branch on every message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are in jambonz's docs or Pipecat's. I had an assumption about the first one and I'd already written it into a public README. It was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The probe
&lt;/h2&gt;

&lt;p&gt;Rather than reason about it, I wrote about 120 lines of aiohttp with two endpoints.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /&lt;/code&gt; is the call hook — jambonz asks it what to do, and it answers with a single &lt;code&gt;listen&lt;/code&gt; verb pointing back at the websocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;verbs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;listen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ws_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mono&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sampleRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidirectionalAudio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streaming&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sampleRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GET /ws&lt;/code&gt; receives the forked audio and, crucially, &lt;strong&gt;describes it rather than interprets it&lt;/strong&gt;: how big each frame is, whether the size is stable, whether any TEXT frames arrive, and the byte total divided by the rate we asked for, against wall clock.&lt;/p&gt;

&lt;p&gt;That last ratio is the whole point. If I ask for 16 kHz and 30 seconds of wall clock produces 30 seconds of implied audio, the rate is right. If it produces 15, the real rate is half what I asked for.&lt;/p&gt;

&lt;p&gt;Then: ngrok, point a jambonz application at it, dial from a softphone, talk for thirty seconds, hang up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;binary frames : 1514
total bytes   : 968960
frame sizes   : [640]
text frames   : 1
wall clock    : 30.03s
audio implied : 30.28s at 16000 Hz, 16-bit mono
ratio         : 1.01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rate the verb asks for is the rate that arrives.&lt;/strong&gt; Ratio 1.01 across thirty seconds. My assumption had been that jambonz forks at 8 kHz — I'd written that in a public README and it was wrong by a factor of two. It forks at whatever &lt;code&gt;sampleRate&lt;/code&gt; the verb set, and the default is 16 kHz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framing is fixed at 20 ms.&lt;/strong&gt; One distinct frame size across 1514 frames: 640 bytes, which is 320 samples of 16-bit mono, which is exactly 20 ms at 16 kHz. No variable-length path needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the third question had a better answer than I expected.&lt;/strong&gt; Exactly one TEXT frame arrives, &lt;em&gt;before any audio&lt;/em&gt;, and then the socket is pure binary for the rest of the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"sampleRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"mixType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"mono"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"callSid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"3e5d869a-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"inbound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"sipStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"callStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"in-progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a &lt;strong&gt;preamble, not a channel&lt;/strong&gt;. So &lt;code&gt;deserialize&lt;/code&gt; doesn't branch per frame. It reads one metadata frame at the start and treats everything after it as L16 PCM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that does to the design
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The sample rate should be read from the wire, not configured.&lt;/strong&gt; My first spec said the verb and the serializer "have to agree" on a rate. They don't have to — the socket states it. A params field can still override for a narrowband carrier, but the default is to believe the connection. A narrowband call at 8 kHz then needs no code change at all.&lt;/p&gt;

&lt;p&gt;I also left it deliberately unset until the preamble arrives, rather than defaulting to 16000. A connection with no preamble then drops audio with a warning, instead of resampling from a rate that was never negotiated — which on a narrowband call produces audio that sounds &lt;em&gt;almost&lt;/em&gt; right, and that is the worst kind of wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The preamble carries &lt;code&gt;callSid&lt;/code&gt; and &lt;code&gt;traceId&lt;/code&gt; for free.&lt;/strong&gt; That's enough to correlate a Pipecat session with a jambonz call in logs, without threading identifiers through the verb's URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And interruptions can't just stop.&lt;/strong&gt; jambonz has &lt;code&gt;killAudio&lt;/code&gt;, which flushes playing and buffered audio. But I asked about the race in the jambonz community Slack and got this from Sam Machin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the ordering of messages on the socket isn't 100% we've seen clients that sometimes prioritise smaller messages so its best to have a small break after a killAudio before you send the next stream, 50-100ms should be plenty&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a frame written in the same instant as the flush can land &lt;em&gt;after&lt;/em&gt; it and play over the caller. Pipecat's interruption model lets a processor stop; over jambonz it has to stop &lt;strong&gt;and wait&lt;/strong&gt;. There's nowhere upstream to put that, so the serializer holds the frames itself for 100 ms — and discards them rather than queueing, because they belong to the turn the caller just interrupted and arriving late is worse than not arriving.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gotcha worth the whole post
&lt;/h2&gt;

&lt;p&gt;I wrote the tests before the serializer, from the measurements. Three of them failed against correct code.&lt;/p&gt;

&lt;p&gt;Pipecat's &lt;code&gt;create_stream_resampler&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; emit one chunk per chunk. It buffers and flushes in bursts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chunk 1: in 640 -&amp;gt; out 0
chunk 2: in 640 -&amp;gt; out 0
chunk 3: in 640 -&amp;gt; out 0
chunk 4: in 640 -&amp;gt; out 3300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three empty returns, then everything at once. My tests assumed one frame in, one frame out — so &lt;code&gt;serialize&lt;/code&gt; returning &lt;code&gt;None&lt;/code&gt; looked like a bug when it was the correct answer. Over twenty frames the throughput settles to about 86% of the arithmetic ideal, the rest being the tail still held in the buffer.&lt;/p&gt;

&lt;p&gt;If you write a serializer for any transport, don't assert a byte count per call. Pump a run of frames and assert on the total, which is what a real call does anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I'd take away
&lt;/h2&gt;

&lt;p&gt;I got the sample rate wrong by assuming, and it reached a public README. I got the test shape wrong by assuming, and it failed against working code. The protocol questions that &lt;em&gt;mattered&lt;/em&gt; were all answered in thirty seconds of one phone call and about 120 lines of throwaway code.&lt;/p&gt;

&lt;p&gt;Writing the probe felt like a detour from writing the serializer. It wasn't — it was the serializer. Everything after it was transcription.&lt;/p&gt;

&lt;p&gt;The probe, the measurements and the spec:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Rehansanjay/jambonz-bridge" rel="noopener noreferrer"&gt;https://github.com/Rehansanjay/jambonz-bridge&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The serializer, in review upstream:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/pipecat-ai/pipecat/pull/6112" rel="noopener noreferrer"&gt;https://github.com/pipecat-ai/pipecat/pull/6112&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>webrtc</category>
    </item>
    <item>
      <title>My tool called a maintainer's plugin broken. They had fixed it two weeks earlier.</title>
      <dc:creator>Rehan Sanjay Venkatesan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:30:47 +0000</pubDate>
      <link>https://dev.to/rehansanjay/my-tool-called-a-maintainers-plugin-broken-they-had-fixed-it-two-weeks-earlier-1jbj</link>
      <guid>https://dev.to/rehansanjay/my-tool-called-a-maintainers-plugin-broken-they-had-fixed-it-two-weeks-earlier-1jbj</guid>
      <description>&lt;p&gt;For about a month I've had a nightly job that installs every community-maintained plugin listed in &lt;a href="https://github.com/pipecat-ai/pipecat" rel="noopener noreferrer"&gt;Pipecat's&lt;/a&gt; docs into a fresh environment, imports it, and checks that every service class it defines can still be constructed. It publishes a table: works, broken, pinned to an old version, can't install.&lt;/p&gt;

&lt;p&gt;It installs from PyPI, because that's what a user gets.&lt;/p&gt;

&lt;p&gt;In late September I opened a pull request against one of the red ones — &lt;code&gt;pipecat-boson&lt;/code&gt;, failing on an import that pipecat moved in 1.8. It sat for a fortnight, and by the time anyone looked at it the maintainer had landed their own fix, so mine was closed. Fair enough; theirs is better than mine was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipecat.utils.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;assert_given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_given&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# pipecat-ai &amp;lt; 1.8.0
&lt;/span&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipecat.services.settings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;assert_given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_given&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That landed on 30 September. PyPI still serves the version published before it. So for more than two weeks my dashboard printed a red row, with their project's name on it, for code that was already correct.&lt;/p&gt;

&lt;p&gt;The check wasn't buggy. It answered exactly the question I wrote it to answer. &lt;strong&gt;The question was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "Broken" was doing two jobs
&lt;/h2&gt;

&lt;p&gt;There are two different sentences hiding in one red row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Your users are getting a package that doesn't work.&lt;/em&gt; True for Boson. Still true today.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Your code is broken.&lt;/em&gt; False for Boson, and false for two weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first is the user's question and the wheel answers it. The second is the maintainer's question, and &lt;strong&gt;the wheel cannot answer it at all&lt;/strong&gt; — a wheel is a snapshot of a repository at some past moment, and nothing says how far behind that moment is.&lt;/p&gt;

&lt;p&gt;This matters more than tidiness, because the two sentences imply different actions. "Your code is broken" asks for a patch. "Your users are getting a broken package" asks for a release. If you only ever send the first one, you're asking people to redo work they've already done, and the thing that would actually help their users never gets mentioned.&lt;/p&gt;

&lt;p&gt;So the checker now asks both. When the released version fails, it installs the plugin's &lt;strong&gt;own source&lt;/strong&gt; from its repository, pinned to the commit under test, and probes that too. A plugin whose source imports and constructs gets a new status — &lt;code&gt;fixed, not released&lt;/code&gt; — which keeps the released failure alongside it and names the commit that passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the repository is the hard part
&lt;/h2&gt;

&lt;p&gt;I assumed this would be a lookup. PyPI has &lt;code&gt;project_urls&lt;/code&gt;. You read &lt;code&gt;Repository&lt;/code&gt;, you clone it, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pipecat-boson&lt;/code&gt; declares nothing.&lt;/strong&gt; No Homepage, no Repository, no &lt;code&gt;project_urls&lt;/code&gt; at all. Its repository appears in exactly one place in the entire package metadata: a &lt;code&gt;git clone&lt;/code&gt; line in the README body.&lt;/p&gt;

&lt;p&gt;And then there's this one, which is worse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage      https://github.com/your-username/pipecat-firered-vad
Repository    https://github.com/your-username/pipecat-firered-vad
Bug Tracker   https://github.com/your-username/pipecat-firered-vad/issues
Changelog     https://github.com/your-username/pipecat-firered-vad/releases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four published URLs are a template placeholder that was never filled in. Anyone who installs that package and hits a problem follows the Bug Tracker link to a 404. Their bug report doesn't go to the wrong place — it goes nowhere, and the maintainer never learns the report existed. There's no route from the package page back to the repository either. I only found the real one because the install error happened to name the dependency.&lt;/p&gt;

&lt;p&gt;So resolution reads the metadata first and falls back to scanning the README. That gets 58 of 67, and every plugin that's currently failing.&lt;/p&gt;

&lt;p&gt;If you maintain a published package: go and look at your own &lt;code&gt;project_urls&lt;/code&gt; right now. It takes ten seconds and the failure mode is silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard matters more than the feature
&lt;/h2&gt;

&lt;p&gt;Here's the part I'd want to read if someone else wrote this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pipecat-supertonic&lt;/code&gt; resolves to &lt;strong&gt;&lt;code&gt;supertone-inc/supertonic&lt;/code&gt;&lt;/strong&gt; — the vendor's own text-to-speech model repository, thirteen thousand stars — because the two names share a word. The plugin actually lives somewhere else entirely.&lt;/p&gt;

&lt;p&gt;That particular one fails to install, so it reports &lt;code&gt;unknown&lt;/code&gt; either way and I got lucky. But a wrong repository that &lt;em&gt;happened&lt;/em&gt; to be pip-installable and to import cleanly would have promoted the plugin to "fixed, not released" on the strength of a completely unrelated codebase, and published that claim under a maintainer's name.&lt;/p&gt;

&lt;p&gt;The fix is that the source build has to produce the distribution the plugin actually is, normalised per PEP 503, or the answer is &lt;code&gt;unknown&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;_canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;built&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;_canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distributions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;built&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;that repository does not build &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measuring something in public means you can be wrong about it in public. The guards are the work, not the measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found, versus what I expected
&lt;/h2&gt;

&lt;p&gt;I expected a pile of these. My assumption going in was that several vendors had quietly merged fixes without cutting releases, which would have given me a stack of genuinely welcome messages to send — &lt;em&gt;your fix is merged, your users still get the broken wheel, a release closes it.&lt;/em&gt; Nobody minds receiving that.&lt;/p&gt;

&lt;p&gt;Thirteen plugins currently fail. Rebuilt from their own source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Still broken when built from source&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in source, stale on PyPI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Couldn't be read, reported &lt;code&gt;unknown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One. Not a stack.&lt;/p&gt;

&lt;p&gt;And the harder number: &lt;strong&gt;zero of the nine vendors has fixed their own plugin.&lt;/strong&gt; I read the HEAD of each repository to check. Two of them looked fixed and were my own false positives — one still imports the removed name from the old path, and the other still uses a frame class that was renamed, which my first grep matched as a substring of the new name.&lt;/p&gt;

&lt;p&gt;So the thing I built to find a pile of easy wins found one, and mostly confirmed that the plugins are broken in source too. That's a worse story and a more useful tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that gained something real
&lt;/h2&gt;

&lt;p&gt;One plugin isn't on PyPI at all under the name its documentation publishes. For a month its row said &lt;code&gt;can't install&lt;/code&gt;, with no diagnosis, because there was nothing to install and therefore nothing to diagnose.&lt;/p&gt;

&lt;p&gt;Built from source, it produces a real one — the same import failure as the others.&lt;/p&gt;

&lt;p&gt;That's the part I'd keep if I could only keep one thing. For a package that was never published, the source isn't a second opinion. It's the only place its state exists at all.&lt;/p&gt;




&lt;p&gt;The checker, the full table and the nightly results:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Rehansanjay/pipecat-plugin-health" rel="noopener noreferrer"&gt;https://github.com/Rehansanjay/pipecat-plugin-health&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you maintain a plugin for a fast-moving framework, I'd genuinely like to know whether you run CI against its &lt;code&gt;main&lt;/code&gt; — and if not, whether it's because nobody asked.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>testing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I sent the fixes. A month later, none of them have merged.</title>
      <dc:creator>Rehan Sanjay Venkatesan</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:41:53 +0000</pubDate>
      <link>https://dev.to/rehansanjay/i-sent-the-fixes-a-month-later-none-of-them-have-merged-2geb</link>
      <guid>https://dev.to/rehansanjay/i-sent-the-fixes-a-month-later-none-of-them-have-merged-2geb</guid>
      <description>&lt;p&gt;A few weeks ago I published a count: of the community plugins listed in Pipecat's docs, 18 of 56 didn't work. Broken imports, missing dependencies, version pins dragging Pipecat backwards a year.&lt;/p&gt;

&lt;p&gt;I also said I'd send PRs for the ones I could fix from outside, and I did — eleven of them, across eleven repositories.&lt;/p&gt;

&lt;p&gt;Here is what has happened since. It is not the post I expected to write, and the most useful part came from a reader rather than from me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eleven PRs
&lt;/h2&gt;

&lt;p&gt;Every one is still open. Every one is mergeable, not a draft, with no conflicts and nothing red.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;29d  ImportError on pipecat &amp;gt;= 1.8        no response
29d  ImportError on pipecat &amp;gt;= 1.8        no response
28d  ImportError on pipecat &amp;gt;= 1.8        1 comment
28d  ImportError on pipecat &amp;gt;= 1.8        no response
28d  ImportError on pipecat &amp;gt;= 1.8        no response
28d  ImportError on pipecat &amp;gt;= 1.8        no response
28d  accept 1.11 patch releases           no response
28d  ImportError on pipecat &amp;gt;= 1.8        no response
22d  InterruptionFrame rename             1 review
22d  LLM context frames removed in 1.8    no response
16d  allow 1.11 and 1.12                  no response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nine of eleven have had no human response at all.&lt;/strong&gt; Nothing has merged.&lt;/p&gt;

&lt;p&gt;I want to be careful about what that does and doesn't mean. A community plugin is usually one engineer inside a vendor, with a roadmap that isn't this. An unreviewed PR from a stranger is one of the easiest things in the world to not get to, and none of these repositories owes me anything. I'm reporting the number because the number is the finding, not because anybody behaved badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then a reader checked my arithmetic
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/arhancanli"&gt;Arhan Canli&lt;/a&gt; commented on the first post within a few hours, and he was right about something I'd got wrong:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pinned five are the only group where nothing has been shown to be broken. A plugin pinned to &lt;code&gt;==1.8.1&lt;/code&gt; fails to resolve, but it may run fine on 1.12.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My headline number lumped together plugins that &lt;strong&gt;fail&lt;/strong&gt; and plugins that merely &lt;strong&gt;won't resolve&lt;/strong&gt;. Those are completely different messages to send a maintainer: one says "your plugin is broken", the other says "loosen a version range."&lt;/p&gt;

&lt;p&gt;He also handed me the test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Install the five with &lt;code&gt;--no-deps&lt;/code&gt; next to the current pipecat-ai, then run the same import and construct step. Any that pass are "pin too tight". Any that fail are real API breaks hiding behind the pin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I ran it.&lt;/p&gt;

&lt;h3&gt;
  
  
  All five were fine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deepdub   pinned &amp;gt;=0.0.97,&amp;lt;0.1.0   -&amp;gt;  compatible
Maya      pinned ==1.8.1           -&amp;gt;  compatible
Memcode   pinned &amp;gt;=1.10,&amp;lt;1.11      -&amp;gt;  compatible
Oruk      pinned &amp;gt;=1.8.1,&amp;lt;1.9      -&amp;gt;  compatible
Wavix     pinned ==0.0.105         -&amp;gt;  compatible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one imports and constructs against pipecat 1.12. &lt;strong&gt;Zero are broken.&lt;/strong&gt; Five one-line edits to a version range would turn five red rows green, and not one of them needs a code change.&lt;/p&gt;

&lt;p&gt;His method needed one adjustment, which is worth passing on. &lt;code&gt;--no-deps&lt;/code&gt; also skips the plugin's &lt;em&gt;own&lt;/em&gt; SDK, so two of the five came back unknown — not because of pipecat, but because their vendor package wasn't installed either. uv's &lt;code&gt;--overrides&lt;/code&gt; is the sharper tool: the plugin resolves with all its real dependencies and only &lt;code&gt;pipecat-ai&lt;/code&gt; is forced to the version under test. That's the question actually being asked, and it turned both unknowns into answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  And a second thing I was getting wrong
&lt;/h2&gt;

&lt;p&gt;Once I was looking properly, one more row didn't belong.&lt;/p&gt;

&lt;p&gt;One plugin imports a vendor SDK that its own README says is &lt;strong&gt;not published to PyPI&lt;/strong&gt; — you contact the company, get a wheel and an API key. Its packaging correctly omits the dependency, because you can't declare one that's on no index.&lt;/p&gt;

&lt;p&gt;That plugin isn't broken. It works for that vendor's customers. It just can't be verified by a checker standing outside the licence — and listing it as "broken" next to nine plugins genuinely broken by an upstream rename put the blame in the wrong place.&lt;/p&gt;

&lt;p&gt;It has its own status now. The checker asks PyPI whether the missing module exists at all, and only calls it gated when the answer is a definite no — a network failure leaves it broken, because reading "I couldn't ask" as "it doesn't exist" would quietly reclassify real breakage as licensing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So the honest number
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;First post&lt;/th&gt;
&lt;th&gt;Today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugins listed&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genuinely broken&lt;/td&gt;
&lt;td&gt;"18"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;— failing on import&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;— can't be installed&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;— partially broken&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works, but pin blocks it&lt;/td&gt;
&lt;td&gt;counted as broken&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works, needs a licensed SDK&lt;/td&gt;
&lt;td&gt;counted as broken&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I published 18 of 56. The defensible figure was never that. &lt;strong&gt;Six of the plugins I called broken were working the whole time&lt;/strong&gt;, and it took a stranger five hours to notice something I'd had weeks to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong, twice
&lt;/h2&gt;

&lt;p&gt;The first write-up assumed the problem was that nobody was looking. Build the checker, publish the list, send the patches, and the table should start going green.&lt;/p&gt;

&lt;p&gt;It didn't. The information existed, the fixes sat one click away, and the count stayed flat. So the gap isn't detection and it isn't authorship — it's that &lt;strong&gt;a community plugin has no owner whose job it is to notice.&lt;/strong&gt; Upstream has correctly delegated it. The vendor shipped it and moved on. The user who hits the ImportError silently picks a different vendor, which is the one signal that would prompt a fix and the one signal nobody receives.&lt;/p&gt;

&lt;p&gt;The second thing I got wrong was subtler and more embarrassing: I built a tool to measure an ecosystem and then didn't interrogate my own categories. "Broken" was doing three jobs — actually broken, won't resolve, and licensed — and I shipped a headline number built on all three.&lt;/p&gt;

&lt;p&gt;If you publish a measurement, someone will check it. That's the best thing that can happen to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that might close the loop
&lt;/h2&gt;

&lt;p&gt;I'd rather end with open questions than a fake conclusion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI against upstream &lt;code&gt;main&lt;/code&gt; in the plugin repo.&lt;/strong&gt; One scheduled workflow, and the break shows up as a red build where it can be fixed. The obvious answer, and almost nobody does it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A health badge on the docs page.&lt;/strong&gt; Puts the signal where the user actually looks, at the cost of whoever maintains the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing.&lt;/strong&gt; It's genuinely possible that a long tail of half-maintained integrations is the normal cost of a fast-moving framework, and users routing around it is the system working rather than failing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain a plugin for any fast-moving framework, I'd like to know whether you run CI against its &lt;code&gt;main&lt;/code&gt;, and if not, why not. My guess is "nobody asked and it wasn't obvious", which would be a very fixable reason.&lt;/p&gt;

&lt;p&gt;The checker, the full table and the nightly results:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Rehansanjay/pipecat-plugin-health" rel="noopener noreferrer"&gt;https://github.com/Rehansanjay/pipecat-plugin-health&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>discuss</category>
      <category>community</category>
    </item>
    <item>
      <title>I installed all 67 Pipecat community plugins. 19 of them don't work.</title>
      <dc:creator>Rehan Sanjay Venkatesan</dc:creator>
      <pubDate>Tue, 13 Oct 2026 20:23:15 +0000</pubDate>
      <link>https://dev.to/rehansanjay/i-installed-all-67-pipecat-community-plugins-19-of-them-dont-work-3ipn</link>
      <guid>https://dev.to/rehansanjay/i-installed-all-67-pipecat-community-plugins-19-of-them-dont-work-3ipn</guid>
      <description>&lt;p&gt;Pipecat's docs list every community-maintained plugin on one page: speech-to-text, text-to-speech, serializers, VAD, analytics, transports. Pick a vendor, &lt;code&gt;pip install&lt;/code&gt;, follow the snippet.&lt;/p&gt;

&lt;p&gt;For 19 of the 67 listed this morning, that doesn't work. The install fails, or the import raises, or the package pins a version of Pipecat from a year ago and quietly drags your whole environment backwards.&lt;/p&gt;

&lt;p&gt;Nobody is hiding this. It's just that nothing was checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the checker does
&lt;/h2&gt;

&lt;p&gt;Every night, for each plugin on the supported-services page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a fresh environment.&lt;/li&gt;
&lt;li&gt;Install the plugin together with &lt;strong&gt;the latest &lt;code&gt;pipecat-ai&lt;/code&gt; release on PyPI&lt;/strong&gt; — what a new user gets today.&lt;/li&gt;
&lt;li&gt;Do it again against &lt;strong&gt;&lt;code&gt;pipecat-ai&lt;/code&gt; main&lt;/strong&gt; — what users get in the next release.&lt;/li&gt;
&lt;li&gt;Import the plugin and construct every Pipecat service class it defines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No API keys are used anywhere, so this doesn't test whether a provider's servers answer. It tests the part that breaks far more often: install, import, and API shape — renamed or removed Pipecat symbols, new abstract methods, version pins.&lt;/p&gt;

&lt;p&gt;The plugin list is re-read from the docs on every run, so new plugins are picked up automatically. That matters: the page had 56 entries three weeks ago and has 67 today.&lt;/p&gt;

&lt;p&gt;Results from this morning's run, against &lt;code&gt;pipecat-ai&lt;/code&gt; 1.12.0:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken on import&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinned to an older Pipecat&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't be installed at all&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The failures cluster, and that's the interesting part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Six plugins die on the same line.&lt;/strong&gt; In Pipecat 1.8 the settings sentinel &lt;code&gt;_NotGiven&lt;/code&gt; was removed from &lt;code&gt;pipecat.services.settings&lt;/code&gt; — it's &lt;code&gt;NotGiven&lt;/code&gt; in &lt;code&gt;pipecat.utils.types&lt;/code&gt; now. Six plugins still import the old name, so they raise before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: cannot import name '_NotGiven' from 'pipecat.services.settings'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six different companies, six separate repositories, one upstream rename. Each fix is a single import line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three more broke on renamed frames.&lt;/strong&gt; One imports &lt;code&gt;StartInterruptionFrame&lt;/code&gt;, another &lt;code&gt;LLMMessagesFrame&lt;/code&gt;, another &lt;code&gt;assert_given&lt;/code&gt; — none of which exist under those names any more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two are missing their own dependency.&lt;/strong&gt; The plugin expects a vendor SDK that isn't installable from where it looks for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five pin Pipecat backwards.&lt;/strong&gt; One requires &lt;code&gt;pipecat-ai&amp;gt;=0.0.97,&amp;lt;0.1.0&lt;/code&gt;. Another wants &lt;code&gt;==0.0.105&lt;/code&gt;. A third pins &lt;code&gt;==1.8.1&lt;/code&gt; exactly. These don't error — which is worse. Your resolver silently downgrades Pipecat, or refuses to solve, and the reason isn't obvious from the traceback you eventually get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three can't be installed at all.&lt;/strong&gt; Two checkouts have neither a &lt;code&gt;pyproject.toml&lt;/code&gt; nor a &lt;code&gt;setup.py&lt;/code&gt;, so there's nothing to install. One isn't on PyPI under the name the docs use.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not upstream's fault
&lt;/h2&gt;

&lt;p&gt;It would be easy to read the &lt;code&gt;_NotGiven&lt;/code&gt; cluster as Pipecat breaking its ecosystem. It isn't.&lt;/p&gt;

&lt;p&gt;The 1.8.0 changelog flagged the change with a ⚠️ and spelled out the migration:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;NOT_GIVEN&lt;/code&gt;, &lt;code&gt;NotGiven&lt;/code&gt;, &lt;code&gt;is_given()&lt;/code&gt; and &lt;code&gt;assert_given()&lt;/code&gt; now come from &lt;code&gt;pipecat.utils.types&lt;/code&gt; and are no longer importable from &lt;code&gt;pipecat.services.settings&lt;/code&gt;. The private &lt;code&gt;pipecat.services.settings._NotGiven&lt;/code&gt; is now the public &lt;code&gt;pipecat.utils.types.NotGiven&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They announced it, named the destination, and were right that the old name was private — plugin authors were importing an underscore-prefixed symbol out of someone else's module, which was always going to be fragile.&lt;/p&gt;

&lt;p&gt;The supported-services page is equally clear about ownership: community plugins are distributed separately and issues go to their own repositories.&lt;/p&gt;

&lt;p&gt;So upstream did the things you'd ask for. The breakage happened anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it keeps happening
&lt;/h2&gt;

&lt;p&gt;Community plugins live in their own repositories, usually maintained by one engineer at the vendor. Pipecat ships often and moves fast, which is a virtue. But a plugin repo has no CI that installs against Pipecat &lt;code&gt;main&lt;/code&gt;, so an upstream rename doesn't produce a red build anywhere — it produces a broken install six weeks later, for a user who has no relationship with either party and will simply choose a different vendor.&lt;/p&gt;

&lt;p&gt;That's the gap: the breakage is public, deterministic and cheap to detect, and there was no place where anyone looked at all of them at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  It measures recovery too
&lt;/h2&gt;

&lt;p&gt;Between two runs in September, one plugin loosened its pin and moved from "needs older Pipecat" to "works". That's the part I didn't expect to enjoy: the table isn't a wall of shame, it's a live picture. Anything on it can move, and most of these are one line away from moving.&lt;/p&gt;

&lt;p&gt;Running against &lt;code&gt;main&lt;/code&gt; as well as the current release is deliberate, and it's the part I'd most like vendors to use. A plugin that breaks against &lt;code&gt;main&lt;/code&gt; today has until the next Pipecat release to fix it, rather than finding out from an issue after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're choosing a plugin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check the pin before you check the features.&lt;/strong&gt; Four of the five pinned plugins will silently downgrade Pipecat in your environment rather than fail, and you'll debug it as something else entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An import test is a five-second question.&lt;/strong&gt; &lt;code&gt;python -c "import pipecat_&amp;lt;name&amp;gt;"&lt;/code&gt; in a fresh environment tells you more than a README.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check when the repo last moved.&lt;/strong&gt; All ten broken plugins have been broken since late August. That's visible from the outside before you commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I've been sending the fixes
&lt;/h2&gt;

&lt;p&gt;Rather than just publishing a list, I opened PRs for the ones I could fix from outside — eleven of them across eleven repositories.&lt;/p&gt;

&lt;p&gt;What happened next is its own post, and it isn't the happy ending: three weeks on, none of them have merged. I'll write that one up separately, because the result surprised me more than the original findings did.&lt;/p&gt;

&lt;p&gt;The repo, with the full table and the nightly results:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Rehansanjay/pipecat-plugin-health" rel="noopener noreferrer"&gt;https://github.com/Rehansanjay/pipecat-plugin-health&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your plugin is on the broken list and you'd rather have a PR than a bug report, say so and I'll send one.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>ai</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
