<?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: Versyka</title>
    <description>The latest articles on DEV Community by Versyka (@getversyka).</description>
    <link>https://dev.to/getversyka</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%2F4200655%2F96f986ac-3e01-40fe-ba6e-e3e6cc16326b.jpg</url>
      <title>DEV Community: Versyka</title>
      <link>https://dev.to/getversyka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/getversyka"/>
    <language>en</language>
    <item>
      <title>A retired Gemini model broke my project, so I built a fix. One month in: 6 users, 50 PRs</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Thu, 12 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/a-retired-gemini-model-broke-my-project-so-i-built-a-fix-one-month-in-6-users-50-prs-2ib5</link>
      <guid>https://dev.to/getversyka/a-retired-gemini-model-broke-my-project-so-i-built-a-fix-one-month-in-6-users-50-prs-2ib5</guid>
      <description>&lt;p&gt;A Gemini model my project depended on was retired in the middle of the project. Nothing warned me until the calls started failing.&lt;/p&gt;

&lt;p&gt;I lost days finding every place it was used, swapping it, and checking that nothing else broke. I build LLM agents for a living, so it wasn't the first time. It was the last straw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody's tools cover
&lt;/h2&gt;

&lt;p&gt;AI vendors now retire models every few months, and SDKs ship breaking versions all the time. The tools we already have don't help much:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependabot and Renovate&lt;/strong&gt; bump a version number. They don't rewrite the code that breaks, and a model ID isn't a package, it's a string in your code, so they never see it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coding agents&lt;/strong&gt; will write a patch if you ask. But you have to know a change is coming, and then you still have to prove the patch is safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing the patch is the easy part now. Knowing what's changing, and proving the fix doesn't break anything, is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; watches your repositories for AI models and APIs that are retiring or changing. When one affects your code, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;finds every affected call, with file and line&lt;/li&gt;
&lt;li&gt;makes the change (an exact model swap is a plain find-and-replace, no AI involved)&lt;/li&gt;
&lt;li&gt;runs your own typecheck, lint, build and tests &lt;strong&gt;before and after&lt;/strong&gt; the change&lt;/li&gt;
&lt;li&gt;opens a pull request only if nothing new fails, with the results attached&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You review and merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first month, honestly
&lt;/h2&gt;

&lt;p&gt;Versyka launched in October. Today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;6 users&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30 repositories&lt;/strong&gt; connected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50 pull requests&lt;/strong&gt; opened, including security fixes and AI model replacements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A senior engineer at Kore.ai let it open security fixes on their code: 3 pull requests covering 39 vulnerabilities, two of them major version jumps. The one they merged would have taken about a day by hand.&lt;/p&gt;

&lt;p&gt;Small numbers, but real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proof beats promises.&lt;/strong&gt; "Tests passed before and after" is what makes an engineer open the diff instead of closing the tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every fix needs AI.&lt;/strong&gt; Swapping a retired model ID for its direct replacement is exact and free. AI is only worth it for breaking API changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tools earn trust first.&lt;/strong&gt; A &lt;a href="https://www.versyka.com/model-retirements#check" rel="noopener noreferrer"&gt;repository check&lt;/a&gt; that needs no account is how most people meet the product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your code calls AI models or third-party APIs, I'd really like to know: what's the last upgrade that broke something for you?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>devops</category>
      <category>startup</category>
    </item>
    <item>
      <title>Would you merge a PR a bot opened? The flaky-or-real check we run first</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/would-you-merge-a-pr-a-bot-opened-the-flaky-or-real-check-we-run-first-3233</link>
      <guid>https://dev.to/getversyka/would-you-merge-a-pr-a-bot-opened-the-flaky-or-real-check-we-run-first-3233</guid>
      <description>&lt;p&gt;"I'd never merge a pull request a bot opened." It's the first objection we hear about automated upgrades, and it's the right instinct. The answer isn't trust, it's evidence: you need to know whether a red test was caused by the change at all. Here's the routine, by hand first.&lt;/p&gt;

&lt;p&gt;A test goes red on a pull request. The usual responses are "rerun the pipeline" or "start debugging". Both skip a cheaper question: is this failure caused by the change at all?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: rerun only the failed test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/test_checkout.py::test_total &lt;span class="nt"&gt;-x&lt;/span&gt;
npx jest &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"total is correct"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fails, then passes:&lt;/strong&gt; flaky. A timeout, a race, a test that depends on order, real network or the current time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fails twice:&lt;/strong&gt; probably real. Keep going.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: run it on main
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git stash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git checkout main
pytest tests/test_checkout.py::test_total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails on main too, your change didn't break it. It was already broken, and that's a separate ticket, not a reason to block this pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: only now, debug
&lt;/h2&gt;

&lt;p&gt;At this point you know the failure is real and new. That's the only kind worth blocking a merge on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why teams skip this
&lt;/h2&gt;

&lt;p&gt;CI shows one colour per run. So the habits are "rerun until green", which hides real failures that happen to pass on a retry, or "block on red", which lets one flaky test hold up every pull request for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baking it into CI
&lt;/h2&gt;

&lt;p&gt;pytest can retry failures and report them separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest-rerunfailures
pytest &lt;span class="nt"&gt;--reruns&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that only passes on the rerun is flagged in the output, so it can be tracked instead of forgotten.&lt;/p&gt;

&lt;p&gt;The stronger version compares results before and after the change: the same tests run on the base commit and on the pull request, and only &lt;em&gt;new&lt;/em&gt; failures count. That's how &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; decides whether an automated upgrade is safe: tests that were already failing are listed in the pull request, a test that fails once and passes on retry is labelled flaky, and only a new, repeatable failure stops the pull request from opening.&lt;/p&gt;

&lt;p&gt;What would you need to see in an automated pull request before you'd merge it?&lt;/p&gt;

</description>
      <category>testing</category>
      <category>cicd</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>chalk and debug shipped malware for a few hours. Waiting 7 days would have skipped it.</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/chalk-and-debug-shipped-malware-for-a-few-hours-waiting-7-days-would-have-skipped-it-255f</link>
      <guid>https://dev.to/getversyka/chalk-and-debug-shipped-malware-for-a-few-hours-waiting-7-days-would-have-skipped-it-255f</guid>
      <description>&lt;p&gt;In September 2025, a maintainer of several very popular npm packages, including &lt;code&gt;chalk&lt;/code&gt; and &lt;code&gt;debug&lt;/code&gt;, had their account phished. New versions were published with code that tried to steal cryptocurrency from browsers. The community spotted it quickly, and the bad versions were removed within hours.&lt;/p&gt;

&lt;p&gt;The teams that got hit were the ones whose installs ran in those hours. Teams that waited even a couple of days never saw the bad versions at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most bad releases have a short life
&lt;/h2&gt;

&lt;p&gt;Not every bad release is an attack. Most are ordinary mistakes: a broken build published by accident, a regression the tests missed, a missing file. The pattern is the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;day 0   release published
day 1   issues opened, maybe a security report
day 2   version deprecated or unpublished, fix released
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a simple rule removes most of the risk: &lt;strong&gt;don't install a release until it's a few days old.&lt;/strong&gt; Seven days is a common choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Renovate&lt;/strong&gt; has it built in:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimumReleaseAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7 days"&lt;/span&gt;&lt;span class="w"&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;&lt;strong&gt;Dependabot&lt;/strong&gt; has a &lt;code&gt;cooldown&lt;/code&gt; option in &lt;code&gt;dependabot.yml&lt;/code&gt; for the same idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly"&lt;/span&gt;
    &lt;span class="na"&gt;cooldown&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lockfiles&lt;/strong&gt; do the rest: with &lt;code&gt;npm ci&lt;/code&gt; and a committed &lt;code&gt;package-lock.json&lt;/code&gt;, nothing new is installed until a pull request changes the lockfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exception: security fixes
&lt;/h2&gt;

&lt;p&gt;A release that patches a known vulnerability is a different trade-off: the risk is in &lt;em&gt;not&lt;/em&gt; installing it. Keep a separate, faster path for security updates, and review them like any other change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do
&lt;/h2&gt;

&lt;p&gt;Versyka's weekly upgrades collect minor and patch updates that are at least 7 days old and open them as one tested pull request, while security fixes take their own path. More in the &lt;a href="https://www.versyka.com/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Does your dependency bot install releases the day they come out?&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Upgrade debt compounds: why three majors behind is a project, not three tasks</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Mon, 09 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/upgrade-debt-compounds-why-three-majors-behind-is-a-project-not-three-tasks-4e17</link>
      <guid>https://dev.to/getversyka/upgrade-debt-compounds-why-three-majors-behind-is-a-project-not-three-tasks-4e17</guid>
      <description>&lt;p&gt;Every team has one: the SDK that's three majors behind, with a ticket that keeps sliding to next sprint.&lt;/p&gt;

&lt;p&gt;It's tempting to think of that as three upgrades' worth of work. It's usually more.&lt;/p&gt;

&lt;p&gt;A real example: a senior engineer at Kore.ai let Versyka open security fixes on their code. It found 39 vulnerabilities across 3 pull requests, and two of the three were major version jumps, the kind that sit in a backlog because nobody has a free day. The one they merged would have taken about a day by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it compounds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Guides assume the previous step.&lt;/strong&gt; The v14 migration guide is written for someone on v13. If you're on v12, half of its "before" examples don't match your code, because v13 already changed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changes stack.&lt;/strong&gt; A method renamed in v13 is removed in v14. The v13 guide's recommended workaround is deprecated in v15. You end up researching history, not just the destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ecosystem moves without you.&lt;/strong&gt; Plugins, type packages and examples drop support for your version. Stack Overflow answers are for the new API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security fixes land on the latest major.&lt;/strong&gt; At some point a vulnerability is fixed only in a version you can't reach without the whole migration. Now the upgrade is urgent and big at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A policy that keeps it small
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stay at most one major behind.&lt;/strong&gt; Upgrade within a few weeks of a new major, after the first patch releases have landed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch up one major at a time.&lt;/strong&gt; v12 → v13, run the tests, merge. Then v13 → v14. Each step has one guide that matches your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the boring part.&lt;/strong&gt; Patch and minor bumps shouldn't need a human. Save attention for breaking changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the gap.&lt;/strong&gt; You can't manage what nobody can see.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For step 4, a quick script works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm outdated &lt;span class="nt"&gt;--long&lt;/span&gt;
pip list &lt;span class="nt"&gt;--outdated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or paste a &lt;code&gt;package.json&lt;/code&gt; / &lt;code&gt;requirements.txt&lt;/code&gt; into Versyka's free &lt;a href="https://www.versyka.com/upgrade-check" rel="noopener noreferrer"&gt;upgrade check&lt;/a&gt;, which shows how many majors behind each API SDK is and links its breaking changes.&lt;/p&gt;

&lt;p&gt;And for step 3 and the breaking upgrades themselves, that's what &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; is for.&lt;/p&gt;

&lt;p&gt;How many majors behind is the oldest SDK in your codebase?&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>devops</category>
      <category>career</category>
    </item>
    <item>
      <title>The Pydantic v1 to v2 cheat sheet (and the trap the deprecation warnings hide)</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Sun, 08 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/the-pydantic-v1-to-v2-cheat-sheet-and-the-trap-the-deprecation-warnings-hide-1kc0</link>
      <guid>https://dev.to/getversyka/the-pydantic-v1-to-v2-cheat-sheet-and-the-trap-the-deprecation-warnings-hide-1kc0</guid>
      <description>&lt;p&gt;Pydantic v2 rewrote the core in Rust and renamed a lot of the public API. Most projects hit the same handful of changes first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renames
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- user.dict()
&lt;/span&gt;&lt;span class="gi"&gt;+ user.model_dump()
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- user.json()
&lt;/span&gt;&lt;span class="gi"&gt;+ user.model_dump_json()
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- User.parse_obj(data)
&lt;/span&gt;&lt;span class="gi"&gt;+ User.model_validate(data)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- User.parse_raw(text)
&lt;/span&gt;&lt;span class="gi"&gt;+ User.model_validate_json(text)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Validators changed shape, not just name
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- from pydantic import validator
&lt;/span&gt;&lt;span class="gi"&gt;+ from pydantic import field_validator
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  class User(BaseModel):
      email: str
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-     @validator("email", pre=True)
-     def clean(cls, v):
&lt;/span&gt;&lt;span class="gi"&gt;+     @field_validator("email", mode="before")
+     @classmethod
+     def clean(cls, v):
&lt;/span&gt;          return v.strip().lower()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pre=True&lt;/code&gt; is &lt;code&gt;mode="before"&lt;/code&gt;, and the validator is an explicit classmethod.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config moved
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  class User(BaseModel):
&lt;span class="gd"&gt;-     class Config:
-         orm_mode = True
&lt;/span&gt;&lt;span class="gi"&gt;+     model_config = ConfigDict(from_attributes=True)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the rename inside it too: &lt;code&gt;orm_mode&lt;/code&gt; is &lt;code&gt;from_attributes&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: it mostly still works
&lt;/h2&gt;

&lt;p&gt;v2 keeps most v1 names as deprecated aliases. So after &lt;code&gt;pip install -U pydantic&lt;/code&gt;, a lot of code keeps running and prints &lt;code&gt;PydanticDeprecatedSince20&lt;/code&gt; warnings. That's helpful for the upgrade and bad for finishing it: the warnings get filtered out, and the work waits for the release that removes them.&lt;/p&gt;

&lt;p&gt;Make them loud in your tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# pytest.ini
&lt;/span&gt;&lt;span class="nn"&gt;[pytest]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;filterwarnings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;error::pydantic.warnings.PydanticDeprecatedSince20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Let the tool do most of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;bump-pydantic
bump-pydantic my_package/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It handles most renames and validator changes. Then run your whole test suite, because behaviour changed in places too (for example, v2 is stricter about some type coercions).&lt;/p&gt;

&lt;p&gt;This loop of rewrite, run the tests, compare with before, is what &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; does for breaking upgrades, and it opens the pull request only if nothing new fails.&lt;/p&gt;

&lt;p&gt;If your requirements still pin &lt;code&gt;pydantic&amp;lt;2&lt;/code&gt;, what's stopping the upgrade?&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>programming</category>
      <category>pydantic</category>
    </item>
    <item>
      <title>0.2.3 doesn't mean what you think: npm's caret below 1.0</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/023-doesnt-mean-what-you-think-npms-caret-below-10-59gi</link>
      <guid>https://dev.to/getversyka/023-doesnt-mean-what-you-think-npms-caret-below-10-59gi</guid>
      <description>&lt;p&gt;The caret in &lt;code&gt;package.json&lt;/code&gt; is so common that most people read it the same way every time: "accept new minors and patches, not majors".&lt;/p&gt;

&lt;p&gt;That's right for versions 1.0 and up. Below 1.0, it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;npm's caret allows changes that don't modify the &lt;strong&gt;leftmost non-zero&lt;/strong&gt; number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^1.4.2  →  &amp;gt;=1.4.2  &amp;lt;2.0.0     (minors and patches)
^0.2.3  →  &amp;gt;=0.2.3  &amp;lt;0.3.0     (patches only)
^0.0.3  →  &amp;gt;=0.0.3  &amp;lt;0.0.4     (that exact version)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check any range yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx semver &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"^0.2.3"&lt;/span&gt; 0.2.9 0.3.0
&lt;span class="c"&gt;# 0.2.9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it works this way
&lt;/h2&gt;

&lt;p&gt;Semantic versioning says that before 1.0, anything may change at any time. npm's compromise is to treat the first non-zero number as the "major". So &lt;code&gt;0.2 → 0.3&lt;/code&gt; is a breaking release as far as your range is concerned, and it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means day to day
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;0.x packages stay put.&lt;/strong&gt; &lt;code&gt;npm update&lt;/code&gt; won't move &lt;code&gt;^0.2.3&lt;/code&gt; to &lt;code&gt;0.3.0&lt;/code&gt;, so a pre-1.0 dependency can fall far behind without anything in your workflow noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bump that "looks minor" isn't.&lt;/strong&gt; When you finally go from &lt;code&gt;0.2.x&lt;/code&gt; to &lt;code&gt;0.3.0&lt;/code&gt;, read the changelog like it's a major. It often is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tilde is stricter still.&lt;/strong&gt; &lt;code&gt;~1.4.2&lt;/code&gt; allows only patches (&lt;code&gt;&amp;lt;1.5.0&lt;/code&gt;). For 0.x, &lt;code&gt;^&lt;/code&gt; and &lt;code&gt;~&lt;/code&gt; end up close to the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick audit
&lt;/h2&gt;

&lt;p&gt;List your direct dependencies that are still pre-1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'const p=require("./package.json");for(const [k,v] of Object.entries({...p.dependencies,...p.devDependencies}))if(/^[\^~]?0\./.test(v))console.log(k,v)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare each against its latest release. Versyka's free &lt;a href="https://www.versyka.com/upgrade-check" rel="noopener noreferrer"&gt;upgrade check&lt;/a&gt; does that comparison for a whole &lt;code&gt;package.json&lt;/code&gt; in one paste, no sign-up.&lt;/p&gt;

&lt;p&gt;How many of your dependencies start with &lt;code&gt;^0&lt;/code&gt;?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>npm</category>
      <category>node</category>
    </item>
    <item>
      <title>Next.js 15 is a one-line upgrade. The breaking changes are in every file that reads a cookie.</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Fri, 06 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/nextjs-15-is-a-one-line-upgrade-the-breaking-changes-are-in-every-file-that-reads-a-cookie-kd7</link>
      <guid>https://dev.to/getversyka/nextjs-15-is-a-one-line-upgrade-the-breaking-changes-are-in-every-file-that-reads-a-cookie-kd7</guid>
      <description>&lt;p&gt;Upgrading to Next.js 15 is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- "next": "14.2.15"
&lt;/span&gt;&lt;span class="gi"&gt;+ "next": "15.0.3"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The work is in the files that line affects. Three changes cause most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Request APIs are async
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt; and &lt;code&gt;draftMode()&lt;/code&gt; now return promises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  import { cookies } from "next/headers";
&lt;span class="err"&gt;
&lt;/span&gt;  export default async function Page() {
&lt;span class="gd"&gt;-   const theme = cookies().get("theme");
&lt;/span&gt;&lt;span class="gi"&gt;+   const theme = (await cookies()).get("theme");
&lt;/span&gt;    ...
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js keeps a temporary synchronous fallback that logs a warning, which is kind for the upgrade and unkind for finding the problem: the code keeps working, and the warning sits in your logs.&lt;/p&gt;

&lt;p&gt;In route params, the same applies: &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; in pages and layouts are promises too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;fetch()&lt;/code&gt; isn't cached by default
&lt;/h2&gt;

&lt;p&gt;In 14, &lt;code&gt;fetch()&lt;/code&gt; in a server component was cached unless you opted out. In 15 it isn't cached unless you opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing breaks. Some pages that were fast because of a cache you never meant to rely on get slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. GET route handlers aren't cached
&lt;/h2&gt;

&lt;p&gt;Same idea for &lt;code&gt;app/**/route.ts&lt;/code&gt;: a &lt;code&gt;GET&lt;/code&gt; handler is dynamic by default now. Add &lt;code&gt;export const dynamic = "force-static"&lt;/code&gt; where you really wanted it cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding every affected file
&lt;/h2&gt;

&lt;p&gt;The official codemod handles most of change 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary upgrade latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then search for what's left, because codemods miss dynamic patterns and helper functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"cookies&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="s2"&gt;|headers&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="s2"&gt;|draftMode&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; app src
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"fetch("&lt;/span&gt; app src | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"cache:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run your build and tests before you call it done. &lt;code&gt;next build&lt;/code&gt; catches type errors from the async change if your project is typed.&lt;/p&gt;

&lt;p&gt;This "the release notes say what changed, but not where it hits &lt;em&gt;your&lt;/em&gt; code" step is the part &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; automates: it reads the release notes against your repository, rewrites the affected calls and runs your checks before and after.&lt;/p&gt;

&lt;p&gt;Have you moved to Next.js 15 yet? What broke first?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>OpenAI's own quickstart still calls a retired model. Your tests won't notice either.</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/openais-own-quickstart-still-calls-a-retired-model-your-tests-wont-notice-either-10jf</link>
      <guid>https://dev.to/getversyka/openais-own-quickstart-still-calls-a-retired-model-your-tests-wont-notice-either-10jf</guid>
      <description>&lt;p&gt;This week we ran our free repository check on &lt;a href="https://github.com/openai/openai-quickstart-python" rel="noopener noreferrer"&gt;openai/openai-quickstart-python&lt;/a&gt;. It still calls &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; in 4 places, a model OpenAI stopped serving on October 23, 2026.&lt;/p&gt;

&lt;p&gt;That's not a dig at OpenAI. It's how this happens in almost every codebase, including ones with good test coverage.&lt;/p&gt;

&lt;p&gt;The suite is green. The pull request merges. The deploy goes out. And every request to the LLM comes back with an error like &lt;code&gt;model_not_found&lt;/code&gt;, because the model ID in the config was retired the night before.&lt;/p&gt;

&lt;p&gt;The tests didn't fail because the tests never talk to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mocking hides it
&lt;/h2&gt;

&lt;p&gt;Mocking the LLM client is the right decision:&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="nd"&gt;@patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.llm.client.chat.completions.create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_create&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mock_create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fake_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A short summary.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A short summary.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's fast, it's free and it's deterministic. But look at what it checks: your code's handling of a response. It never checks that the request would be accepted. The model ID is just a string that goes into a mock and disappears.&lt;/p&gt;

&lt;p&gt;So when OpenAI retired &lt;code&gt;gpt-4-0613&lt;/code&gt; on October 23, a repository still sending it stayed green right up until real traffic hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cheap checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Put model IDs in one place.&lt;/strong&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="c1"&gt;# config.py
&lt;/span&gt;&lt;span class="n"&gt;SUMMARY_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.6-sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;CLASSIFIER_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If model names are scattered across ten files, no check will find them all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check them against shutdown dates in CI, not against the API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vendors publish retirement dates months ahead. A CI step that reads your config and compares it to a list of dates fails the build before the date, with no API key and no network call to the model. (Versyka's &lt;a href="https://www.versyka.com/integrations#github-action" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; does this, and the &lt;a href="https://www.versyka.com/model-retirements" rel="noopener noreferrer"&gt;retirement calendar&lt;/a&gt; has the dates as JSON if you'd rather script it.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. One real smoke call per model on deploy.&lt;/strong&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SUMMARY_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLASSIFIER_MODEL&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&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;role&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;user&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;content&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;ping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A one-token request per model costs almost nothing and fails the deploy, not the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next date
&lt;/h2&gt;

&lt;p&gt;Claude Sonnet 4.5's dated ID, &lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;, shuts down on November 30. If it's in your repository, that's a good one to test these checks on.&lt;/p&gt;

&lt;p&gt;Where do the model names live in your codebase: one config file, or scattered through the code?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your OpenAI streaming loop works in the demo. It breaks on the last chunk.</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Wed, 04 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/your-openai-streaming-loop-works-in-the-demo-it-breaks-on-the-last-chunk-354e</link>
      <guid>https://dev.to/getversyka/your-openai-streaming-loop-works-in-the-demo-it-breaks-on-the-last-chunk-354e</guid>
      <description>&lt;p&gt;If you moved from the OpenAI Python SDK 0.28 to 1.x, your non-streaming code probably broke loudly: &lt;code&gt;openai.ChatCompletion.create&lt;/code&gt; became &lt;code&gt;client.chat.completions.create&lt;/code&gt;, and every &lt;code&gt;response["choices"][0]["message"]["content"]&lt;/code&gt; became &lt;code&gt;response.choices[0].message.content&lt;/code&gt;. You fixed those in an afternoon.&lt;/p&gt;

&lt;p&gt;Streaming code is different. It usually doesn't break at all. It just gets slightly wrong, on the very last chunk, after the user has already read the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: &lt;code&gt;content&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In 0.28, chunks were dicts, and most people wrote:&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;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&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;content&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing key gave you an empty string. In 1.x, chunks are typed objects, and the straight translation is:&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;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;   &lt;span class="c1"&gt;# TypeError on the final chunk
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;delta.content&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; on the final chunk (the one that carries &lt;code&gt;finish_reason&lt;/code&gt;) and on chunks that carry tool calls instead of text. If you build the text with an f-string or &lt;code&gt;str()&lt;/code&gt;, you don't get an error. You get the word "None" at the end of the user's answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the chunk with no choices
&lt;/h2&gt;

&lt;p&gt;Turn on usage reporting:&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stream_options&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;include_usage&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the last chunk carries &lt;code&gt;usage&lt;/code&gt; with an empty &lt;code&gt;choices&lt;/code&gt; list. &lt;code&gt;chunk.choices[0]&lt;/code&gt; raises &lt;code&gt;IndexError&lt;/code&gt; on the final chunk, after the user has already seen a complete answer. In logs it looks like a random failure at the end of a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# usage-only final chunk
&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;or ""&lt;/code&gt; covers the &lt;code&gt;None&lt;/code&gt;, and &lt;code&gt;continue&lt;/code&gt; skips the usage chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;Most test suites mock the LLM with one hand-written chunk. Both traps live on the first or last chunk of a real stream, so a mock never reaches them. The only test I trust for streaming reads a real stream (or a recorded one) to the end and checks the joined text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding every place it hides
&lt;/h2&gt;

&lt;p&gt;Streaming loops tend to get copy-pasted: a chat endpoint, a background summarizer, a CLI script. Search for &lt;code&gt;.delta.content&lt;/code&gt; and &lt;code&gt;choices[0]&lt;/code&gt; across the whole repository, not only the file you remember. That search, plus rewriting what it finds and running your tests before and after, is what &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt; automates when an SDK ships a breaking change.&lt;/p&gt;

&lt;p&gt;What's the strangest bug a streaming response has given you? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>programming</category>
      <category>openai</category>
    </item>
    <item>
      <title>API deprecation: shut down v2 without stranding your customers</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/api-deprecation-shut-down-v2-without-stranding-your-customers-5c2c</link>
      <guid>https://dev.to/getversyka/api-deprecation-shut-down-v2-without-stranding-your-customers-5c2c</guid>
      <description>&lt;p&gt;Every API team that has shut down a version knows the curve. A quick drop in v2 traffic after the announcement, then a long, flat tail of customers who never move. Shutdown day arrives, support gets the calls, and the date moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tail doesn't move
&lt;/h2&gt;

&lt;p&gt;It isn't that customers didn't read the email. They did. They added a ticket. The ticket competes with their own roadmap, and "rewrite working code to call a newer endpoint" loses every sprint planning.&lt;/p&gt;

&lt;p&gt;The cost of migrating sits in the customer's codebase, and nothing you send changes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually helps, in order of effort
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A migration guide with exact before and after code&lt;/strong&gt; for each SDK language. Not a list of changed fields, actual code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codemods&lt;/strong&gt;: scripts that rewrite the common call patterns. Customers still have to find, run and review them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response headers and SDK warnings&lt;/strong&gt; (&lt;code&gt;Deprecation&lt;/code&gt;, &lt;code&gt;Sunset&lt;/code&gt;, a log line on every v2 call), so the developer sees it, not just the account owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A migration in their repository&lt;/strong&gt;: a pull request with their calls rewritten and their tests run. Merging is a review, not a project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step moves more of the tail, because each one moves more of the work off the customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing step 4 at scale
&lt;/h2&gt;

&lt;p&gt;That's the provider program in &lt;a href="https://www.versyka.com/for-api-providers" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Share your install page.&lt;/strong&gt; You get &lt;code&gt;versyka.com/install/your-company&lt;/code&gt; to put in your changelog, upgrade emails and docs. Customers who install from it opt in; nothing touches a repository without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the change once.&lt;/strong&gt; Paste the migration guide you already write; it becomes a precise rewrite rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers get tested pull requests.&lt;/strong&gt; Every opted-in repository that uses your SDK gets one, with &lt;strong&gt;the customer's own tests run&lt;/strong&gt;. It's free for them: you sponsor it, and nothing merges without their review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch adoption.&lt;/strong&gt; The portal shows pull requests opened, merged and repositories not affected, per change. Counts only: customer names stay private.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plans start at $1,500 a month on an annual contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even if you never use a tool for it
&lt;/h2&gt;

&lt;p&gt;Put the before and after code in your guide. Send the deprecation signal through the SDK, not only the inbox. And count the tail by repository, not by account. That last number tells you what shutdown day will look like.&lt;/p&gt;

&lt;p&gt;If you've run a version shutdown, I'd like to know what moved your long tail. Comments are open.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>saas</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why we price a developer tool per fix instead of per seat</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Mon, 02 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/why-we-price-a-developer-tool-per-fix-instead-of-per-seat-2gna</link>
      <guid>https://dev.to/getversyka/why-we-price-a-developer-tool-per-fix-instead-of-per-seat-2gna</guid>
      <description>&lt;p&gt;When we priced &lt;a href="https://www.versyka.com" rel="noopener noreferrer"&gt;Versyka&lt;/a&gt;, per seat was the obvious default. Almost every developer tool does it.&lt;/p&gt;

&lt;p&gt;It didn't fit, for three reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A bot doesn't have seats
&lt;/h2&gt;

&lt;p&gt;Versyka opens pull requests. The people who review them might be one platform engineer or the whole team. Charging for each of them would mean charging more because more people can look at a PR, which is backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The value is in the fix, not the access
&lt;/h2&gt;

&lt;p&gt;What a team actually wants is a breaking upgrade done: the code rewritten, the tests passing, a PR to review. An afternoon of an engineer's time that didn't get spent. That's the unit worth paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A failed attempt shouldn't cost you
&lt;/h2&gt;

&lt;p&gt;If the change can't pass your tests, you didn't get anything. So it isn't counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a fix
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;migration pull request&lt;/strong&gt;: a breaking upgrade where Versyka changed your code and your tests passed.&lt;/p&gt;

&lt;p&gt;What doesn't count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version upgrades&lt;/strong&gt; with no code changes. Unlimited on every plan (with a fair-use cap on daily test runs), because they're cheap to run: no AI, just your tests in a sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replacing a retiring AI model&lt;/strong&gt; with its direct replacement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attempts that never passed&lt;/strong&gt; your tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seats&lt;/strong&gt;, from the Team plan up: everyone with write access signs in, no seat fees. Free and Starter include you and two teammates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The plans
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Fixes / month&lt;/th&gt;
&lt;th&gt;Per fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;2 (5 repositories)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;$4.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team&lt;/td&gt;
&lt;td&gt;$249/mo&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;$4.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;$749/mo&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;$3.75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The trade-off we accepted
&lt;/h2&gt;

&lt;p&gt;Usage pricing is harder to predict than seats, for us and for you. Two things help: the monthly allowance is a ceiling (nothing surprise-bills), and the dashboard shows how many fixes are left this month before any work starts.&lt;/p&gt;

&lt;p&gt;I'd like to hear from people who've bought or built usage-priced dev tools: what made the unit feel fair, and what made it feel like a meter running?&lt;/p&gt;

</description>
      <category>startup</category>
      <category>saas</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Fail CI when a pull request adds a retired OpenAI or Anthropic model</title>
      <dc:creator>Versyka</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/getversyka/fail-ci-when-a-pull-request-adds-a-retired-openai-or-anthropic-model-30c7</link>
      <guid>https://dev.to/getversyka/fail-ci-when-a-pull-request-adds-a-retired-openai-or-anthropic-model-30c7</guid>
      <description>&lt;p&gt;A retired AI model doesn't fail your tests, because your tests mock the LLM. It fails in production, the day after the shutdown date.&lt;/p&gt;

&lt;p&gt;So check model IDs where you check everything else: on the pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/models.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI models&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# for the PR comment&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;versyka/versyka-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No account, no API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on a PR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A retired model&lt;/strong&gt; (say &lt;code&gt;gpt-4-0613&lt;/code&gt;): the check fails, and a comment on the PR says which file and line, and what to use instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model retiring within 90 days&lt;/strong&gt;: the check passes, with a heads-up in the comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything current&lt;/strong&gt;: the check passes quietly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comment is updated in place on each push, so the PR doesn't fill up with bot messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;versyka/versyka-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;soon&lt;/span&gt;     &lt;span class="c1"&gt;# none | retired (default) | soon | behind&lt;/span&gt;
          &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;          &lt;span class="c1"&gt;# how far ahead counts as "soon" (default 90)&lt;/span&gt;
          &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;     &lt;span class="c1"&gt;# comment on the PR (default true)&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;services/api&lt;/span&gt;  &lt;span class="c1"&gt;# folder to check, for monorepos (default .)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fail-on: soon&lt;/code&gt; is useful the month before a big shutdown date: it turns the heads-up into a blocker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outputs
&lt;/h2&gt;

&lt;p&gt;The step exposes &lt;code&gt;retired&lt;/code&gt;, &lt;code&gt;soon&lt;/code&gt; and &lt;code&gt;behind&lt;/code&gt; as outputs, so you can branch on them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;models&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;versyka/versyka-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.models.outputs.soon != '0'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Models retiring soon&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.models.outputs.soon }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the dates come from
&lt;/h2&gt;

&lt;p&gt;The same retirement data behind &lt;a href="https://www.versyka.com/model-retirements" rel="noopener noreferrer"&gt;versyka.com/model-retirements&lt;/a&gt;, with dates from OpenAI, Anthropic, Google, Mistral, Cohere, AWS Bedrock, Azure, Groq and Together AI.&lt;/p&gt;

&lt;p&gt;The action is on the &lt;a href="https://github.com/marketplace/actions/versyka-ai-model-retirements-and-sdk-upgrades" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>ai</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
