<?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: Alan West</title>
    <description>The latest articles on DEV Community by Alan West (@alanwest).</description>
    <link>https://dev.to/alanwest</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%2F3834047%2F6413d0cf-9d90-4ccc-80a9-123656fd78ba.png</url>
      <title>DEV Community: Alan West</title>
      <link>https://dev.to/alanwest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alanwest"/>
    <language>en</language>
    <item>
      <title>How to Run Old Programs on Windows 11 (Even 16-Bit Apps From 1985)</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Tue, 27 Oct 2026 20:20:02 +0000</pubDate>
      <link>https://dev.to/alanwest/how-to-run-old-programs-on-windows-11-even-16-bit-apps-from-1985-3ngp</link>
      <guid>https://dev.to/alanwest/how-to-run-old-programs-on-windows-11-even-16-bit-apps-from-1985-3ngp</guid>
      <description>&lt;p&gt;The 32-bit FreeCell that Microsoft bundled with Win32s still runs on a current Windows 11 install. No patches, no emulator. You double-click &lt;code&gt;FREECELL.EXE&lt;/code&gt;, dated 22-Feb-93, and deal a hand. enlyth &lt;a href="https://blog.enlyth.com/posts/oldest-windows-gui-app-win-11/" rel="noopener noreferrer"&gt;went looking for the oldest GUI app that still launches on Windows 11&lt;/a&gt;, and that was the clean winner at 33 years and 8 months old.&lt;/p&gt;

&lt;p&gt;If you allow one helper, the record goes back further. &lt;code&gt;REVERSI.EXE&lt;/code&gt; from Windows 1.01, dated 15-Nov-1985, launches on Windows 11 through otvdm. So a 1985 app runs on a 2026 OS. Your accounting tool from 2004 still refuses to start, though, and that's the part worth explaining.&lt;/p&gt;

&lt;p&gt;If an old program won't run on Windows 11 and compatibility mode did nothing, start here. Below: why old software fails, a PowerShell one-liner that tells you which kind of failure you have, and the fixes in the order I'd try them: compatibility settings, missing runtimes (VB6, DirectX, .NET 3.5), otvdm for 16-bit apps, DOSBox, and a VM as the last resort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why old software won't start on Windows 11
&lt;/h2&gt;

&lt;p&gt;Windows 11 only ships as a 64-bit OS, and 64-bit Windows has never run 16-bit code. Microsoft's own KB is blunt about it: &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/x64-windows-not-support-16-bit-programs" rel="noopener noreferrer"&gt;x64 versions of Windows don't support 16-bit programs, 16-bit processes, or 16-bit components&lt;/a&gt;. NTVDM, the piece that ran DOS and Win16 apps on 32-bit NT, &lt;a href="https://learn.microsoft.com/en-us/windows/compatibility/ntvdm-and-16-bit-app-support" rel="noopener noreferrer"&gt;was never included in 64-bit editions&lt;/a&gt; and isn't supported on x64 or ARM at all.&lt;/p&gt;

&lt;p&gt;That one rule explains a lot of the "it worked on XP" reports. The other usual causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 16-bit installer for a 32-bit app. The program would run fine, but the &lt;code&gt;SETUP.EXE&lt;/code&gt; that installs it is 16-bit.&lt;/li&gt;
&lt;li&gt;Missing runtimes: VB6 controls, old DirectX DLLs like &lt;code&gt;d3dx9_43.dll&lt;/code&gt;, .NET Framework 3.5, old Visual C++ redistributables.&lt;/li&gt;
&lt;li&gt;Admin assumptions: apps that write into &lt;code&gt;C:\Program Files&lt;/code&gt; or &lt;code&gt;HKLM&lt;/code&gt; because nobody stopped them on Windows 98.&lt;/li&gt;
&lt;li&gt;Display assumptions: 256-colour palettes, fixed 640x480 layouts, no DPI awareness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The display one sounds cosmetic until it bites. In the &lt;a href="https://news.ycombinator.com/item?id=50248055" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt;, mattbee describes a Windows 3.1 kids' game that started on Windows 11 but drew a mostly black screen, because its 256-colour palette changes no longer worked. Fixing it took a virtual GDI layer, plus fixes for a 32-bit disk-space overflow and threading bugs that only appeared on multi-core CPUs. So "still launches" is a pretty low bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step zero: check if the EXE is 16-bit, 32-bit or DOS
&lt;/h2&gt;

&lt;p&gt;Don't start ticking boxes in the Compatibility tab until you know whether the EXE is DOS, 16-bit Windows or 32-bit Windows. The executable header tells you. Paste this into PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IO.File&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReadAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"C:\old\APP.EXE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x40&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BitConverter&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x3C&lt;/span&gt;&lt;span class="p"&gt;)&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="kr"&gt;else&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="mi"&gt;0&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-le&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&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="s2"&gt;"MZ only: DOS program"&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="kr"&gt;else&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="nv"&gt;$sig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Text.Encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ASCII.GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PE"&lt;/span&gt;&lt;span class="p"&gt;)&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="s2"&gt;"PE, machine 0x{0:X4}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BitConverter&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToUInt16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&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="kr"&gt;else&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="nv"&gt;$sig&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="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;How to read the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NE&lt;/code&gt; means 16-bit Windows (Windows 1.x to 3.x era). Go to otvdm below.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PE, machine 0x014C&lt;/code&gt; means 32-bit x86. This should run natively, so the problem is a dependency, permissions or the installer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PE, machine 0x8664&lt;/code&gt; means 64-bit. Not an age problem.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MZ only&lt;/code&gt;, &lt;code&gt;LE&lt;/code&gt;, or two random characters usually means DOS or a DOS extender (plain DOS EXEs often have garbage at offset 0x3C). Go to DOSBox.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Error messages and their usual fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Unsupported 16-Bit Application"&lt;/td&gt;
&lt;td&gt;Win16 or DOS binary&lt;/td&gt;
&lt;td&gt;otvdm (Win16) or DOSBox (DOS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup fails, the app itself is 32-bit&lt;/td&gt;
&lt;td&gt;16-bit InstallShield stub&lt;/td&gt;
&lt;td&gt;Microsoft's 16-bit InstallShield Support package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MSCOMCTL.OCX&lt;/code&gt; / component not registered&lt;/td&gt;
&lt;td&gt;VB6 extended control missing&lt;/td&gt;
&lt;td&gt;Copy the OCX and register it with the 32-bit &lt;code&gt;regsvr32&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;d3dx9_43.dll&lt;/code&gt; not found&lt;/td&gt;
&lt;td&gt;Legacy DirectX SDK runtime&lt;/td&gt;
&lt;td&gt;DirectX End-User Runtimes (June 2010)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt to install .NET Framework 3.5&lt;/td&gt;
&lt;td&gt;App targets .NET 1.1 to 3.5&lt;/td&gt;
&lt;td&gt;Enable .NET 3.5, or use the standalone installer on 26H1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crashes saving settings&lt;/td&gt;
&lt;td&gt;Writes to protected paths&lt;/td&gt;
&lt;td&gt;Run as administrator, or install outside Program Files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiny, blurry or clipped UI&lt;/td&gt;
&lt;td&gt;No DPI awareness&lt;/td&gt;
&lt;td&gt;High DPI override&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1. Compatibility mode, and why it usually isn't enough
&lt;/h3&gt;

&lt;p&gt;Right-click the EXE, open &lt;strong&gt;Properties &amp;gt; Compatibility&lt;/strong&gt;. On Windows 11 you get &lt;a href="https://pureinfotech.com/run-old-app-compatibility-mode-windows-11/" rel="noopener noreferrer"&gt;compatibility modes from Windows 95 to Windows 8&lt;/a&gt;, plus reduced color mode (8-bit or 16-bit), 640x480, and "Run this program as an administrator". Microsoft's guided version is under &lt;strong&gt;Settings &amp;gt; System &amp;gt; Troubleshoot &amp;gt; Other troubleshooters &amp;gt; Program Compatibility Troubleshooter&lt;/strong&gt; (&lt;a href="https://support.microsoft.com/en-us/windows/make-older-apps-or-programs-compatible-with-windows-783d6dd7-b439-bdb0-0490-54eea0f45938" rel="noopener noreferrer"&gt;steps&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This tab is overrated. Picking "Windows XP (Service Pack 3)" helps apps that check the OS version and bail out, and it smooths over some permission problems. It doesn't install a missing DLL, and it can't turn a 16-bit binary into a 32-bit one. Two boxes in it do more real work than the version dropdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run as administrator&lt;/strong&gt;, for apps that insist on writing next to their own EXE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change high DPI settings &amp;gt; Override high DPI scaling behavior&lt;/strong&gt;, then try "System" or "System (Enhanced)" for old UIs that look broken on a 150% scaled laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the app ran on Windows 7, try the "Windows 7" mode first, then work backwards.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install the runtime it actually needs
&lt;/h3&gt;

&lt;p&gt;In practice a missing dependency is a far more common culprit than a version check.&lt;/p&gt;

&lt;p&gt;With VB6 apps, the core runtime (&lt;code&gt;msvbvm60.dll&lt;/code&gt; and friends) ships in Windows 11, and Microsoft says it's &lt;a href="https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-basic-6/visual-basic-6-support-policy" rel="noopener noreferrer"&gt;supported for the lifetime of the OS&lt;/a&gt;. The controls are another matter. Files like &lt;code&gt;mscomctl.ocx&lt;/code&gt;, &lt;code&gt;comdlg32.ocx&lt;/code&gt; and &lt;code&gt;msflxgrd.ocx&lt;/code&gt; are "extended files" that each app was supposed to ship itself, and plenty of old installers did that badly. They're 32-bit, so register them from an elevated prompt with the 32-bit &lt;code&gt;regsvr32&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;copy&lt;/span&gt; &lt;span class="kd"&gt;MSCOMCTL&lt;/span&gt;.OCX &lt;span class="kd"&gt;C&lt;/span&gt;:\Windows\SysWOW64\
&lt;span class="kd"&gt;C&lt;/span&gt;:\Windows\SysWOW64\regsvr32.exe &lt;span class="kd"&gt;C&lt;/span&gt;:\Windows\SysWOW64\MSCOMCTL.OCX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some controls, like &lt;code&gt;threed32.ocx&lt;/code&gt; and &lt;code&gt;graph32.ocx&lt;/code&gt;, are on Microsoft's unsupported list. If your app depends on them, you're on your own.&lt;/p&gt;

&lt;p&gt;Old games that throw &lt;code&gt;d3dx9_*.dll&lt;/code&gt;, XInput 1.3 or XAudio 2.7 errors need Microsoft's &lt;a href="https://www.microsoft.com/en-us/download/details.aspx?id=8109" rel="noopener noreferrer"&gt;DirectX End-User Runtimes (June 2010)&lt;/a&gt;. It's &lt;code&gt;directx_Jun2010_redist.exe&lt;/code&gt;, about 95.6 MB, and it doesn't touch the DirectX version built into Windows. Get it from Microsoft, not from a "dll download" site.&lt;/p&gt;

&lt;p&gt;For .NET 1.1 to 3.5 apps, on most Windows 11 builds you can tick ".NET Framework 3.5" in &lt;strong&gt;Turn Windows features on or off&lt;/strong&gt;. That's changing: starting with &lt;a href="https://learn.microsoft.com/en-us/dotnet/framework/install/dotnet-35-windows" rel="noopener noreferrer"&gt;Windows 11 26H1 (build 28000)&lt;/a&gt;, .NET 3.5 is only available as a standalone installer and can't be added as a Windows component. It still works, it's just one more step that a 2009 app's installer won't know about.&lt;/p&gt;

&lt;p&gt;Last case: the app is 32-bit and only the installer is 16-bit InstallShield, install Microsoft's &lt;a href="https://download.microsoft.com/download/748580b8-0e66-45c5-a4b0-dbd37a44e230/16BitInstallShieldSupport.msi" rel="noopener noreferrer"&gt;16-bit InstallShield Support package&lt;/a&gt; first, then run setup again.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. otvdm (winevdm): run 16-bit Windows apps on 64-bit Windows 11
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/otya128/winevdm" rel="noopener noreferrer"&gt;otvdm&lt;/a&gt; is a modified winevdm, the Wine-based 16-bit Windows emulator, ported to 64-bit Windows and licensed under GPL-2.0. It's how REVERSI.EXE from 1985 runs on Windows 11. Setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download a release from the GitHub releases page.&lt;/li&gt;
&lt;li&gt;Right-click &lt;code&gt;install.inf&lt;/code&gt; and choose &lt;strong&gt;Install&lt;/strong&gt; (or run the "install" shortcut).&lt;/li&gt;
&lt;li&gt;Double-click your Win16 EXE as usual. You can also drag it onto &lt;code&gt;otvdm.exe&lt;/code&gt;, or launch &lt;code&gt;otvdmw.exe&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it complains about &lt;code&gt;VCRUNTIME140.dll&lt;/code&gt;, install the 32-bit Visual C++ 2017 redistributable. If a Windows update resets the registry hooks, run the install step again. To remove it, run &lt;code&gt;uninstall.reg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;enlyth said using it "does feel like cheating", and that's fair. From the user's side it's the closest thing to the NTVDM that 64-bit Windows never had. The README is clear that its DOS support is incomplete and sends you to DOSBox for DOS programs.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. DOSBox for DOS programs
&lt;/h3&gt;

&lt;p&gt;DOS apps have needed an emulator on 64-bit Windows since the start. Use &lt;a href="https://www.dosbox-staging.org/" rel="noopener noreferrer"&gt;DOSBox Staging&lt;/a&gt; or DOSBox-X rather than the original DOSBox, which has gone years between releases. Basic use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;mount&lt;/span&gt; &lt;span class="kd"&gt;c&lt;/span&gt; &lt;span class="kd"&gt;C&lt;/span&gt;:\dos
&lt;span class="kd"&gt;c&lt;/span&gt;:
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="kd"&gt;WP51&lt;/span&gt;
&lt;span class="kd"&gt;wp&lt;/span&gt;&lt;span class="err"&gt;.exe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. A VM, last
&lt;/h3&gt;

&lt;p&gt;Microsoft's official workaround for 16-bit software is to &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/x64-windows-not-support-16-bit-programs" rel="noopener noreferrer"&gt;dual-boot a 32-bit Windows on a separate disk&lt;/a&gt;. Please don't. Put Windows XP or 32-bit Windows 10 in Hyper-V or VirtualBox, give it no network unless it really needs one, and snapshot it. Both are past end of support, so treat the VM like a museum exhibit and keep it off the internet. Microsoft itself recommends turning NTVDM off in enterprise environments for security reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cutoff is 16-bit code
&lt;/h2&gt;

&lt;p&gt;The 1993 FreeCell result shows how deep Win32 compatibility goes. Anything 32-bit has a decent chance, and anything 16-bit needs a translator. If you maintain one of these apps at work, the order above (check the header, fix the runtime, then emulate) will get most of them running in an afternoon. Leaving one running for another ten years is a separate decision, and probably a worse one.&lt;/p&gt;

&lt;p&gt;So what's the oldest program you still run every day, and what did it take to keep it alive? And if you think the VM should be the first step, not the last, I'd like to hear why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;enlyth, &lt;a href="https://blog.enlyth.com/posts/oldest-windows-gui-app-win-11/" rel="noopener noreferrer"&gt;Oldest GUI application that still runs on Windows 11&lt;/a&gt; and the &lt;a href="https://news.ycombinator.com/item?id=50248055" rel="noopener noreferrer"&gt;HN discussion&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/x64-windows-not-support-16-bit-programs" rel="noopener noreferrer"&gt;64-bit Windows doesn't support 16-bit programs (KB896458)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/windows/compatibility/ntvdm-and-16-bit-app-support" rel="noopener noreferrer"&gt;NTVDM and 16-bit app support&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-basic-6/visual-basic-6-support-policy" rel="noopener noreferrer"&gt;Support statement for Visual Basic 6.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/dotnet/framework/install/dotnet-35-windows" rel="noopener noreferrer"&gt;Install .NET Framework 3.5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://www.microsoft.com/en-us/download/details.aspx?id=8109" rel="noopener noreferrer"&gt;DirectX End-User Runtimes (June 2010)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/otya128/winevdm" rel="noopener noreferrer"&gt;otya128/winevdm on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>windows</category>
      <category>tutorial</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Self-Hosted LLM vs API Cost: Below $10.5K a Month, an H200 Node Can't Beat the API</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Tue, 27 Oct 2026 19:00:01 +0000</pubDate>
      <link>https://dev.to/alanwest/self-hosted-llm-vs-api-cost-below-105k-a-month-an-h200-node-cant-beat-the-api-20</link>
      <guid>https://dev.to/alanwest/self-hosted-llm-vs-api-cost-below-105k-a-month-an-h200-node-cant-beat-the-api-20</guid>
      <description>&lt;p&gt;IBM's CEO told investors last week that open-weight models now run at "anywhere from 25 percent to 10 percent of the token cost of the frontier models" and are "90 percent as effective." According to &lt;a href="https://www.theregister.com/systems/2026/10/22/ibm-ceo-says-open-weight-models-are-now-irresistibly-good-and-cheap-on-prem/5304640" rel="noopener noreferrer"&gt;The Register's write-up of the Q3 call&lt;/a&gt;, Arvind Krishna also thinks a useful on-prem AI setup needs just two or three racks.&lt;/p&gt;

&lt;p&gt;I wanted to know if self-hosted LLM vs API cost actually works out like that, so I took public GPU prices, power rates, published throughput benchmarks and today's API price sheets and did the arithmetic. Short version: an 8x H200 node costs about $10.5K a month before anyone touches it. Against Claude Opus 5.5 it pays for itself at 6% utilization. Against DeepSeek's own off-peak API it needs 54%. If your API bill is under about $10.5K a month, the API beats that node no matter what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Krishna's own benchmark says 71%, not 90%
&lt;/h2&gt;

&lt;p&gt;On the call he described a broad benchmark where the top closed model scored 70 and open models scored 20 or 30 in 2024, then "70 to 50" by late 2025. 50 out of 70 is 71%.&lt;/p&gt;

&lt;p&gt;Maybe the gap has closed further since then, but the only figures he actually gave don't reach 90%. That matters because everything below assumes the open model can do your job, and if it can't, the cost per token is irrelevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking 10-25% against Claude, GPT and DeepSeek API prices
&lt;/h2&gt;

&lt;p&gt;Current frontier list prices, next to the best-known hosted open-weight model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input $/1M&lt;/th&gt;
&lt;th&gt;Output $/1M&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;$2&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-6.1-sol&lt;/td&gt;
&lt;td&gt;$2&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://developers.openai.com/api/docs/pricing" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro (peak)&lt;/td&gt;
&lt;td&gt;$1.32&lt;/td&gt;
&lt;td&gt;$3.96&lt;/td&gt;
&lt;td&gt;&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;DeepSeek&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro (off-peak)&lt;/td&gt;
&lt;td&gt;$0.66&lt;/td&gt;
&lt;td&gt;$1.98&lt;/td&gt;
&lt;td&gt;&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;DeepSeek&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4.1 Flash (peak)&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;DeepSeek&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;V4 Pro output at peak is 19.8% of Opus 5.5 and 39.6% of Sonnet 5.5. So "10-25%" holds if you compare against the most expensive tier and ignore the mid tier. That's fair enough for an earnings call, but most teams aren't sending everything to Opus.&lt;/p&gt;

&lt;p&gt;That's API to API, though. IBM sells hardware and Red Hat, so the claim it cares about is on-prem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an 8x H200 node costs per month
&lt;/h2&gt;

&lt;p&gt;The inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardware: an 8x H200 HGX server goes for about $340,000 new in volume (&lt;a href="https://www.amcompute.com/gpu-server-values/h200" rel="noopener noreferrer"&gt;Rack Report via AMCompute&lt;/a&gt;, October 2026). Spread over 36 months, that's $9,444/month.&lt;/li&gt;
&lt;li&gt;Power: that page puts the server at 10.2 kW. 10.2 kW × 720 h = 7,344 kWh. At the US commercial average of 14.47¢/kWh (&lt;a href="https://eia.gov/electricity/monthly/update/end-use.php" rel="noopener noreferrer"&gt;EIA, August 2026&lt;/a&gt;), that's about $1,063/month before cooling overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That comes to $10,507/month, without colo, networking, spares or people.&lt;/p&gt;

&lt;p&gt;As a sanity check, DeepSeek published its own fleet cost in February 2025: $87,072/day across an average of 226.75 eight-GPU H800 nodes, assuming $2/GPU-hour (&lt;a href="https://github.com/deepseek-ai/open-infra-index/blob/main/202502OpenSourceWeek/day_6_one_more_thing_deepseekV3R1_inference_system_overview.md" rel="noopener noreferrer"&gt;DeepSeek open-infra-index&lt;/a&gt;). That's about $384 per node per day, or $11,520 per month, close enough to my number.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many tokens that node actually produces
&lt;/h2&gt;

&lt;p&gt;This is where people argue most, because published throughput numbers vary by about 8x depending on who tuned the stack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Untuned, one node: Verda ran DeepSeek-V3 FP8 on 8x H200 with SGLang v0.4.1, 1024 tokens in and 1024 out, and got 2,864 output tok/s at 8 requests/s (&lt;a href="https://verda.com/blog/deepseek-v3-llm-nvidia-h200-gpu-inference-benchmarking" rel="noopener noreferrer"&gt;Verda&lt;/a&gt;). They say themselves it had no H200-specific tuning.&lt;/li&gt;
&lt;li&gt;Tuned, multi-node: LMSYS ran DeepSeek with prefill/decode disaggregation and large-scale expert parallelism on 12 nodes of 8x H100, and reported 22.3k output tok/s per node with 2000-token inputs. They put the cost at $0.20 per 1M output tokens (&lt;a href="https://lmsys.org/blog/2025-05-05-large-scale-ep/" rel="noopener noreferrer"&gt;LMSYS&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Production: DeepSeek's own fleet averaged about 14.8k output tok/s per H800 node while decoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over a 30-day month (2,592,000 seconds), the Verda figure works out to 7.42B output tokens, plus the same number of input tokens. The LMSYS figure works out to 57.8B.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break-even utilization vs each API
&lt;/h2&gt;

&lt;p&gt;I priced one fully loaded month of the untuned Verda workload (7.42B in + 7.42B out) at each API's rates, then divided the node's $10,507 by that. The result is the utilization the node needs just to break even:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compared against&lt;/th&gt;
&lt;th&gt;API cost of the same tokens&lt;/th&gt;
&lt;th&gt;Break-even utilization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;$178,164&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 / gpt-6.1-sol&lt;/td&gt;
&lt;td&gt;$89,082&lt;/td&gt;
&lt;td&gt;11.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro, peak&lt;/td&gt;
&lt;td&gt;$39,196&lt;/td&gt;
&lt;td&gt;26.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro, off-peak&lt;/td&gt;
&lt;td&gt;$19,598&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the untuned setup at 100% load, self-hosting costs $1.41 per 1M output tokens, with the matching input tokens included. At the LMSYS throughput it's $0.18, which lines up with their $0.20 claim. Compared with Opus 5.5's $20, that's 7% and 0.9%, which is well below Krishna's range.&lt;/p&gt;

&lt;p&gt;So at enterprise volume he's being conservative. If the open model really does the job and you can keep the hardware busy, on-prem beats the frontier API easily.&lt;/p&gt;

&lt;p&gt;The off-peak row is the one to stare at. DeepSeek's peak window is only 01:00-04:00 and 06:00-10:00 UTC on weekdays, so most of the week is billed at the lower rate. And these are cache-miss input prices. Cache hits on V4 Pro are $0.022-0.044 per 1M, so an agent that resends the same long context is cheaper again on the API side.&lt;/p&gt;

&lt;p&gt;One caveat on the table: Verda benchmarked V3, not V4 Pro. I'm using it as a stand-in for a large open MoE on one node. Your model and prompt lengths will move these numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where self-hosting falls apart for small teams
&lt;/h2&gt;

&lt;p&gt;Look at the fixed cost again: $10,507/month before anyone touches it. If your whole API bill is under about $10.5K/month, a node can't win, whatever its utilization. And DeepSeek's own API at off-peak rates needs you above 53.6% load before your hardware pays off.&lt;/p&gt;

&lt;p&gt;Then there are the people. Krishna said it himself: "Where are my kernel engineers? Where are the people who worry about the underlying network?" The Register notes he "didn't have answers" beyond IBM building software to help. I'm not putting a salary in the table because it varies too much, but every dollar of engineering time you spend on the node adds a dollar to the break-even bill. If babysitting the node costs about as much as the node itself, the break-even roughly doubles to around $21K/month.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://news.ycombinator.com/item?id=50238088" rel="noopener noreferrer"&gt;HN thread&lt;/a&gt; mostly agrees. jacquesm, who runs an 8-GPU rig that cost about $25K, says it "is not cheaper than using a frontier lab (or at least, I don't think it is)." Forgeties79 says: "I can't 'invest' $25k into an LLM. The vast majority of people can't." The people defending local models mostly bring up privacy, no token caps and no refusals. Hardly anyone argues on price.&lt;/p&gt;

&lt;h2&gt;
  
  
  A single RTX 5090 dev box: a much lower bar
&lt;/h2&gt;

&lt;p&gt;One RTX 5090 is a different calculation. &lt;a href="https://www.hardware-corner.net/gpu-llm-benchmarks/rtx-5090/" rel="noopener noreferrer"&gt;Hardware Corner&lt;/a&gt; lists the card at $3,699 (average US market value) and 575 W TDP, and measured these llama.cpp generation speeds at 4K context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen3 32B Q4_K: 61.4 tok/s (43.8 at 32K)&lt;/li&gt;
&lt;li&gt;Qwen3 30B A3B Q4_K: 226.1 tok/s (76.8 at 128K)&lt;/li&gt;
&lt;li&gt;gpt-oss 20B Q4_K: 298.2 tok/s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over 36 months the card costs $103/month. Run it flat out 8 hours a day, 22 days a month, and power adds about $15 at the same EIA rate. So the break-even is roughly $117/month of API spend you'd actually stop paying, assuming you already have the rest of the PC.&lt;/p&gt;

&lt;p&gt;That's easy to hit with agentic coding, which burns huge amounts of input tokens. $117 buys 58M input tokens on Sonnet 5.5. It's also why the cheap tiers are hard to beat. At full speed, 176 hours a month on Qwen3 32B is about 38.9M output tokens. At Haiku 5.5's $0.50/1M output (prompts up to 100K tokens), that's worth $19. The dev box only saves money if it replaces mid- or top-tier calls, and one 5090 owner on HN (dekhn) says the local models "generate an enormous amount of noise" next to Claude or ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do before you buy GPUs
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pull last month's bill and split it by model tier. Only spending on Sonnet- and Opus-class models is worth trying to replace.&lt;/li&gt;
&lt;li&gt;Rent a node by the hour (H100s run about $2.65/GPU-hour on-demand at &lt;a href="https://www.spheron.network/blog/nvidia-h100-price-2026/" rel="noopener noreferrer"&gt;Spheron&lt;/a&gt;) and benchmark with your real prompt shape:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm bench serve &lt;span class="nt"&gt;--model&lt;/span&gt; &amp;lt;your-model&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dataset-name&lt;/span&gt; random &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--random-input-len&lt;/span&gt; 1024 &lt;span class="nt"&gt;--random-output-len&lt;/span&gt; 1024 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request-rate&lt;/span&gt; 8 &lt;span class="nt"&gt;--num-prompts&lt;/span&gt; 2400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run your own eval set against the open model. Don't trust benchmark leaderboards, and don't trust "90%."&lt;/li&gt;
&lt;li&gt;Break-even utilization = monthly node cost ÷ (monthly tokens at full load × your API price). If you can't keep the node above that number during your real traffic hours, keep paying for the API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My take: IBM's math is right for companies that already run data centers and have steady, boring workloads like the email summaries and contract checks Krishna mentioned. For a 10-person team paying $3K a month, it's a sales pitch for hardware.&lt;/p&gt;

&lt;p&gt;So where did it tip for you? At what monthly API bill did self-hosting start making sense, if it ever did? And if you run your own node, what utilization do you actually see outside office hours?&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/systems/2026/10/22/ibm-ceo-says-open-weight-models-are-now-irresistibly-good-and-cheap-on-prem/5304640" rel="noopener noreferrer"&gt;The Register: IBM CEO says open weight models are now good and cheap on-prem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50238088" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Anthropic API pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/pricing" rel="noopener noreferrer"&gt;OpenAI API pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;DeepSeek API pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepseek-ai/open-infra-index/blob/main/202502OpenSourceWeek/day_6_one_more_thing_deepseekV3R1_inference_system_overview.md" rel="noopener noreferrer"&gt;DeepSeek V3/R1 inference system overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lmsys.org/blog/2025-05-05-large-scale-ep/" rel="noopener noreferrer"&gt;LMSYS: large-scale expert parallelism on 96 H100s&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://verda.com/blog/deepseek-v3-llm-nvidia-h200-gpu-inference-benchmarking" rel="noopener noreferrer"&gt;Verda: DeepSeek-V3 on 8x H200 benchmark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amcompute.com/gpu-server-values/h200" rel="noopener noreferrer"&gt;AMCompute: HGX H200 server values&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hardware-corner.net/gpu-llm-benchmarks/rtx-5090/" rel="noopener noreferrer"&gt;Hardware Corner: RTX 5090 LLM benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eia.gov/electricity/monthly/update/end-use.php" rel="noopener noreferrer"&gt;EIA Electric Power Monthly, end-use prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.spheron.network/blog/nvidia-h100-price-2026/" rel="noopener noreferrer"&gt;Spheron: H100 price 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>selfhosted</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>GPT-6.1 Terra vs Claude Opus 5.5: 15x Cheaper per Task. Here's the Catch</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:00:03 +0000</pubDate>
      <link>https://dev.to/alanwest/gpt-61-terra-vs-claude-opus-55-15x-cheaper-per-task-heres-the-catch-42d</link>
      <guid>https://dev.to/alanwest/gpt-61-terra-vs-claude-opus-55-15x-cheaper-per-task-heres-the-catch-42d</guid>
      <description>&lt;p&gt;On Artificial Analysis, GPT-6.1 Terra at max effort costs $0.40 per task and Claude Opus 5.5 costs $5.98. That's about 15x. Opus also scores 58 on their Intelligence Index to Terra's 48.1, and those ten points are the gap OpenAI's launch charts work hard to keep you from seeing.&lt;/p&gt;

&lt;p&gt;Terra shipped on October 26. Everyone is googling the same comparison this week, so I collected the published numbers for GPT-6.1 Terra vs GPT-6 (Astra, plus 6.1 Sol in between) vs Claude Opus 5.5 into one place: OpenAI's &lt;a href="https://openai.com/index/introducing-gpt-6-1-terra/" rel="noopener noreferrer"&gt;launch post&lt;/a&gt;, the model and pricing pages, Artificial Analysis, and the &lt;a href="https://news.ycombinator.com/item?id=50248124" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt;. I didn't run anything myself. Every number below comes from a source I link to.&lt;/p&gt;

&lt;h2&gt;
  
  
  GPT-6.1 Terra vs Sol vs GPT-6 Astra vs Opus 5.5: pricing and benchmarks
&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;GPT-6.1 Terra&lt;/th&gt;
&lt;th&gt;GPT-6.1 Sol&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;Claude Opus 5.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input / output per 1M tokens&lt;/td&gt;
&lt;td&gt;$1 / $5&lt;/td&gt;
&lt;td&gt;$2 / $10&lt;/td&gt;
&lt;td&gt;$10 / $50&lt;/td&gt;
&lt;td&gt;$4 / $20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached input per 1M&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;see note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max output&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;128K (300K via Batch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA Intelligence Index (max)&lt;/td&gt;
&lt;td&gt;48.1&lt;/td&gt;
&lt;td&gt;51.8&lt;/td&gt;
&lt;td&gt;52.7&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA cost per task (max)&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;td&gt;$0.72&lt;/td&gt;
&lt;td&gt;$3.26&lt;/td&gt;
&lt;td&gt;$5.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA time per task (max)&lt;/td&gt;
&lt;td&gt;384s&lt;/td&gt;
&lt;td&gt;543s&lt;/td&gt;
&lt;td&gt;399s&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA Terminal-Bench 4.0&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;59.6%&lt;/td&gt;
&lt;td&gt;59.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA hallucination rate&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: OpenAI pricing and model pages via &lt;a href="https://www.eesel.ai/blog/gpt-6-1-terra" rel="noopener noreferrer"&gt;eesel's roundup&lt;/a&gt; and &lt;a href="https://cellcog.ai/blog/gpt-6-1-terra/" rel="noopener noreferrer"&gt;cellcog&lt;/a&gt;, Artificial Analysis via &lt;a href="https://www.eesel.ai/blog/gpt-6-1-terra" rel="noopener noreferrer"&gt;eesel&lt;/a&gt;, &lt;a href="https://benchlm.ai/models/gpt-6-1-terra" rel="noopener noreferrer"&gt;BenchLM&lt;/a&gt; and &lt;a href="https://www.deeplearning.ai/the-batch/claude-opus-5-5-leaps-forward" rel="noopener noreferrer"&gt;The Batch&lt;/a&gt;. Opus 5.5 cache pricing differs between write-ups, so check Anthropic's page before you budget around it.&lt;/p&gt;

&lt;p&gt;A few things the table leaves out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terra's model ID is &lt;code&gt;gpt-6.1-terra&lt;/code&gt;. Knowledge cutoff is June 1, 2026.&lt;/li&gt;
&lt;li&gt;In ChatGPT, Terra is available to Plus, Pro, Enterprise and Edu users in ChatGPT Work and Codex. It's also in the API, which has no free tier.&lt;/li&gt;
&lt;li&gt;Prompts over 272K input tokens cost $2 / $7.50, and that rate applies to the whole request. Batch and Flex cost half of standard: $0.50 / $2.50.&lt;/li&gt;
&lt;li&gt;Terra costs half what its predecessor GPT-5.6 Terra did on input and well under half on output. GPT-5.6 Terra was $2 / $12.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where OpenAI's charts flatter Terra
&lt;/h2&gt;

&lt;p&gt;OpenAI's headline claim is that at max effort Terra is more than 3x as token-efficient as Opus 5.5 at a quarter of the per-token price, which it says works out to roughly 9x lower cost per task. That comes from Agents' Last Exam, and OpenAI's own chart gives the numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agents' Last Exam (max)&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Est. cost per task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6.1 Terra&lt;/td&gt;
&lt;td&gt;60.0%&lt;/td&gt;
&lt;td&gt;$1.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;
&lt;td&gt;58.6%&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;59.3%&lt;/td&gt;
&lt;td&gt;$7.24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;63.2%&lt;/td&gt;
&lt;td&gt;$8.82&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cost math holds up. The trouble is that OpenAI picked the benchmark where Terra lands closest to Opus. Look at what else is published:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terminal-Bench 4.0: OpenAI reports Terra at 50.3% and compares it only with GPT-5.6 Terra (21.5%), not with Opus. Anthropic &lt;a href="https://rits.shanghai.nyu.edu/ai/claude-opus-5-5-takes-the-top-spot-on-artificial-analysis-at-58/" rel="noopener noreferrer"&gt;reports 66.4%&lt;/a&gt; for Opus 5.5. On Artificial Analysis's harness it's 55.6% for Terra and 59.6% for Opus. The two harnesses differ, so don't mix their numbers, but Terra trails on both.&lt;/li&gt;
&lt;li&gt;Knowledge and science: on AA's numbers, Terra gets 46.2% on HLE to Opus's 61.4%, and 55.1% on SciCode to Opus's 66.9%. Neither appears in the launch post.&lt;/li&gt;
&lt;li&gt;Missing comparisons: commenters on HN noticed that Opus appears only at Max and never at lower effort levels. It's also missing from the OSWorld chart, and Anthropic models are absent from the factual-error chart. OSWorld 2.1 and MMMU-Pro are compared only with OpenAI's own models: on computer use, Terra at medium scores 15 points above GPT-5.6 Terra and "on par with GPT-6 Astra", and on MMMU-Pro it beats GPT-5.6 Terra at every reasoning setting and "nearly matches" Astra and Sol.&lt;/li&gt;
&lt;li&gt;Terminal-Bench-Science: OpenAI says Terra comes within five points of Opus 5.5 at less than a fifth of the estimated cost. That's a loss with a coupon attached.&lt;/li&gt;
&lt;li&gt;More than three times faster than Astra and Sol: AA's max-effort timing has Terra at 384s per task and Astra at 399s. That's 4% faster. OpenAI's 3x figure comes from its own task runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the thread, OpenAI's tedsanders pointed out that eval error bars are "commonly like ±5%." Fair enough, but by that measure Terra's 3.2-point ALE deficit to Opus is inside the noise too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terra's effort levels: where cost per task flips the ranking
&lt;/h2&gt;

&lt;p&gt;Here Terra earns its spot. If you sort by score, Opus wins. If you sort by score per dollar, Terra's lower effort levels win, and by a lot. Artificial Analysis breaks Terra down by effort:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terra effort&lt;/th&gt;
&lt;th&gt;AA Index&lt;/th&gt;
&lt;th&gt;Cost per task&lt;/th&gt;
&lt;th&gt;Time per task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;33.5&lt;/td&gt;
&lt;td&gt;$0.11&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;36.2&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;29s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;41.3&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;63s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;43.9&lt;/td&gt;
&lt;td&gt;$0.13&lt;/td&gt;
&lt;td&gt;96s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xhigh&lt;/td&gt;
&lt;td&gt;45.4&lt;/td&gt;
&lt;td&gt;$0.19&lt;/td&gt;
&lt;td&gt;160s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;48.1&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;td&gt;384s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AA calls low and medium Pareto-optimal. Going from xhigh to max roughly doubles the cost for 2.7 points. Opus 5.5's cheapest setting, low effort, is listed at $0.55 per task and scores 42 on the index. Terra at medium gets 41.3 for $0.10. In the other direction, Opus at medium already scores 51, above Terra at max, but &lt;a href="https://artificialanalysis.ai/models/releases/claude-opus-5-5" rel="noopener noreferrer"&gt;AA's Opus page&lt;/a&gt; doesn't publish a cost for that setting, so I can't tell you what it costs per task.&lt;/p&gt;

&lt;p&gt;Third-party evals point the same way. chriddyp's Plotly data-analytics benchmark has Terra joining "the Pareto Frontier in between GPT-6 Luna and GPT-6.1 Sol," and of 33 models tested, only Haiku 5.5 and Nemotron 3.5 Super VL finished faster (both scored lower). thefourthchime's PacBench gave Terra 84 points for $0.19 in 5.4 minutes, against Grok 4.7's 83 for $1.69 in 36.5 minutes.&lt;/p&gt;

&lt;p&gt;Terra at max is harder to defend. zozbot234 argued in the thread that Terra Max is beaten by Sol High on both price per task and intelligence. Sol at max already scores 51.8 for $0.72. So if you're about to run Terra at max, try Sol first, and if you want Terra, run it at medium.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people using it report
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;jl&lt;/strong&gt; runs a production app almost entirely on the OpenAI API and called Terra "an AWESOME release" for cost-sensitive agentic work. They still use Claude for coding.&lt;/li&gt;
&lt;li&gt;agluszak said Terra, like Luna, "will stop at an earliest excuse." That's the lazy-agent problem again.&lt;/li&gt;
&lt;li&gt;mapontosevenths ran one prompt through Codex (on Sol, not Terra) and Claude. Claude used 3.01x the tokens (649,147 vs 141,160) and wrote 2.25x more code. The same user said OpenAI had cut usage on its $200 plan by about 50% and called "cost per task" a "weaselly way" to hide that.&lt;/li&gt;
&lt;li&gt;On the other side, hansonkd wrote: "I hate Opus 5.5, constant overreach and over-engineering for any project." If that's how Opus behaves for you, Terra's lower token count looks like a feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One number I'd watch: OpenAI's &lt;a href="https://deploymentsafety.openai.com/gpt-6-1-terra" rel="noopener noreferrer"&gt;system card addendum&lt;/a&gt; has a test where the model, mid-task, finds a message board where other agents post. Terra took the specified unauthorized action in 13.75% of samples (11 of 80). Sol did it in 1.25%. If you're wiring Terra into a multi-agent setup with shell access, keep that in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you switch to GPT-6.1 Terra?
&lt;/h2&gt;

&lt;p&gt;My opinion: Terra is the new default for high-volume agent loops, classification, ticket triage and tool-calling pipelines, where you pay per call and 41 on the AA index at medium is plenty. For hard coding work, long refactors and anything where a wrong answer costs more than $5.98, it doesn't replace Opus 5.5.&lt;/p&gt;

&lt;p&gt;If you want to try it without committing, pin the effort level explicitly. Medium is the API default today, but defaults change and max costs four times as much:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.openai.com/v1/responses &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-6.1-terra",
    "reasoning": { "effort": "medium" },
    "input": "Classify this support ticket: ..."
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="nt"&gt;-m&lt;/span&gt; gpt-6.1-terra &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;model_reasoning_effort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you switch, check these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run your own 20-50 real tasks at Terra medium, Terra max and Sol high. Compare cost per &lt;em&gt;successful&lt;/em&gt; task, since that's the figure that shows up on your bill.&lt;/li&gt;
&lt;li&gt;Log input sizes. If your prompts regularly go past 272K tokens, the whole request is billed at $2 / $7.50. Sol has the same surcharge, so moving up a tier won't dodge it.&lt;/li&gt;
&lt;li&gt;Use Batch/Flex ($0.50 / $2.50) for anything that doesn't need an answer within seconds.&lt;/li&gt;
&lt;li&gt;Keep Opus 5.5 (&lt;code&gt;claude-opus-5-5&lt;/code&gt;) behind a fallback for the tasks Terra gives up on early.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: are you switching your default model to Terra, or are a few points on OpenAI's chosen benchmarks too small to matter for your work? And if you've run Terra on a real codebase, did it finish the job or "stop at an earliest excuse"? Tell me in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openai.com/index/introducing-gpt-6-1-terra/" rel="noopener noreferrer"&gt;OpenAI: Introducing GPT-6.1 Terra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50248124" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.eesel.ai/blog/gpt-6-1-terra" rel="noopener noreferrer"&gt;eesel: GPT-6.1 Terra pricing, benchmarks (OpenAI pricing page + Artificial Analysis data)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://the-decoder.com/gpt-6-1-terra-halves-openais-api-prices-and-still-gains-ground-on-its-larger-models/" rel="noopener noreferrer"&gt;The Decoder: GPT-6.1 Terra halves OpenAI's API prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cellcog.ai/blog/gpt-6-1-terra/" rel="noopener noreferrer"&gt;CellCog: Terra vs Sol and Opus (OpenAI ALE chart)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://benchlm.ai/models/gpt-6-1-terra" rel="noopener noreferrer"&gt;BenchLM: GPT-6.1 Terra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.deeplearning.ai/the-batch/claude-opus-5-5-leaps-forward" rel="noopener noreferrer"&gt;The Batch: Claude Opus 5.5 Leaps Forward&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://artificialanalysis.ai/models/releases/claude-opus-5-5" rel="noopener noreferrer"&gt;Artificial Analysis: Claude Opus 5.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://deploymentsafety.openai.com/gpt-6-1-terra" rel="noopener noreferrer"&gt;OpenAI: GPT-6.1 Terra system card addendum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rits.shanghai.nyu.edu/ai/claude-opus-5-5-takes-the-top-spot-on-artificial-analysis-at-58/" rel="noopener noreferrer"&gt;RITS NYU: Claude Opus 5.5 takes the top spot on Artificial Analysis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>openai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>vLLM Tool Calling Not Working? 1 in 12 Calls Fail by Default. Here's the Fix</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:10:02 +0000</pubDate>
      <link>https://dev.to/alanwest/vllm-tool-calling-not-working-1-in-12-calls-fail-by-default-heres-the-fix-hnp</link>
      <guid>https://dev.to/alanwest/vllm-tool-calling-not-working-1-in-12-calls-fail-by-default-heres-the-fix-hnp</guid>
      <description>&lt;p&gt;If your vLLM tool calling is not working with Kimi or DeepSeek, and your agent keeps emitting broken JSON, inventing arguments or looping on the same call, check your server before you blame the model. Ubicloud measured Kimi K2.6 on vLLM 0.28 and got schema-valid tool calls &lt;a href="https://www.ubicloud.com/blog/fixing-tool-call-accuracy-in-vllm" rel="noopener noreferrer"&gt;only 92% of the time&lt;/a&gt;. That is 2 failures in every 25 calls, roughly 1 in 12.&lt;/p&gt;

&lt;p&gt;With constrained decoding turned on, Ubicloud got to 100%. vLLM doesn't turn it on for you, even though Moonshot, who make the model, &lt;a href="https://github.com/MoonshotAI/K2-Vendor-Verifier#suggestions-to-vendors" rel="noopener noreferrer"&gt;tell vendors to use it&lt;/a&gt; (they call it guided encoding). On vLLM 0.31+ the fix is one server flag, &lt;code&gt;--tool-strict-level parameter&lt;/code&gt;, and the rest of this post is about why and how to check it worked.&lt;/p&gt;

&lt;p&gt;A lot of the "open models are bad at agents" talk I've seen comes down to a server default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broken vLLM tool calls look like
&lt;/h2&gt;

&lt;p&gt;On the client it rarely says "tool call failed". Instead you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;arguments&lt;/code&gt; that won't parse as JSON, or parse but miss a required field&lt;/li&gt;
&lt;li&gt;a parameter the tool never declared, or an enum value that isn't in the enum&lt;/li&gt;
&lt;li&gt;the agent reading back its own validation error, retrying with the same mistake and burning your token budget in a loop&lt;/li&gt;
&lt;li&gt;a framework that quietly drops the call and lets the model answer in prose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these looks like a model problem, so people swap models or add three paragraphs of "ALWAYS RETURN VALID JSON" to the system prompt. Neither fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  92% sounds fine until you chain calls
&lt;/h2&gt;

&lt;p&gt;Per-call accuracy compounds. At 92%, a 10-step agent run gets through clean 0.92^10 ≈ 43% of the time. At 20 steps it's about 19%. In the &lt;a href="https://news.ycombinator.com/item?id=50242870" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt;, one commenter worked out that even 95% per call gives you about an 8% chance of finishing a 50-call sequential task. Someone else in the same thread said they run around 300 tool calls per prompt.&lt;/p&gt;

&lt;p&gt;92% wasn't a fluke of Ubicloud's setup, either. Moonshot's own &lt;a href="https://github.com/MoonshotAI/K2-Vendor-Verifier" rel="noopener noreferrer"&gt;K2 Vendor Verifier&lt;/a&gt; results (test date 2025-11-15) show the vLLM reference deployment at 87.22% schema accuracy on K2-thinking and 76.00% on K2-0905, while Moonshot's API and providers like Fireworks hit 100% serving the same weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why vLLM leaves tool calls unconstrained by default
&lt;/h2&gt;

&lt;p&gt;Without constraints, the model samples tool-call tokens like any other text. Kimi K2.6's format looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;|tool_calls_section_begin|&amp;gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;|tool_call_begin|&amp;gt;functions.webfetch:0&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;|tool_call_argument_begin|&amp;gt;{"url": "https://ubicloud.com", "format": "markdown"}&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;|tool_call_end|&amp;gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;|tool_calls_section_end|&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing stops the model from writing an unknown function name, a non-integer index or JSON that ignores your schema. Constrained decoding uses &lt;a href="https://xgrammar.mlc.ai/docs/structural_tag/structural_tag_api.html" rel="noopener noreferrer"&gt;XGrammar structural tags&lt;/a&gt; to mask the vocabulary at each step, so once the model opens a tool call it can only produce a declared tool name, then an integer index, then JSON that matches that tool's schema, then the end tag.&lt;/p&gt;

&lt;p&gt;vLLM supports this, just not on every request. &lt;a href="https://github.com/vllm-project/vllm/pull/45600" rel="noopener noreferrer"&gt;PR #45600&lt;/a&gt;, merged in June 2026, changed &lt;code&gt;tool_choice="auto"&lt;/code&gt; so structural tags apply only when at least one tool sets &lt;code&gt;"strict": true&lt;/code&gt;. A missing &lt;code&gt;strict&lt;/code&gt; counts as not strict. A reviewer on that PR objected that Claude Code's protocol doesn't include a &lt;code&gt;strict&lt;/code&gt; field at all, which left Messages API clients with no obvious opt-in; the PR ended up adding &lt;code&gt;strict&lt;/code&gt; to vLLM's Anthropic tool model. Plenty of OpenAI-style agent code doesn't set it either.&lt;/p&gt;

&lt;p&gt;So if you're on &lt;code&gt;tool_choice="auto"&lt;/code&gt; (most agents are) without &lt;code&gt;strict&lt;/code&gt; on your tools, you're sampling free-form. Ubicloud originally added constrained decoding in their own vLLM fork. After vLLM shipped the feature in v0.25 they switched to upstream, and their score dropped back to 91% because of this default change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: &lt;code&gt;--tool-strict-level&lt;/code&gt; in vLLM 0.31+
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.31.0" rel="noopener noreferrer"&gt;vLLM 0.31.0&lt;/a&gt; added a server-side floor, &lt;code&gt;--tool-strict-level&lt;/code&gt; (&lt;a href="https://github.com/vllm-project/vllm/pull/56268" rel="noopener noreferrer"&gt;PR #56268&lt;/a&gt;). From the &lt;a href="https://docs.vllm.ai/en/latest/features/tool_calling.html" rel="noopener noreferrer"&gt;tool calling docs&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;What gets constrained&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default for most parsers. Structural tags only for &lt;code&gt;required&lt;/code&gt;/named tool choice, or &lt;code&gt;auto&lt;/code&gt; when a tool sets &lt;code&gt;strict: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;function&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tool-call envelope (markup, function name) on every request with tools. Arguments stay free unless the tool is &lt;code&gt;strict&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parameter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everything in &lt;code&gt;function&lt;/code&gt;, plus argument schemas for every tool, as if all were strict&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't set it, the parser's default applies: &lt;code&gt;function&lt;/code&gt; for &lt;code&gt;glm47&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt; for everything else, Kimi included.&lt;/p&gt;

&lt;p&gt;For a Kimi deployment I'd go straight to &lt;code&gt;parameter&lt;/code&gt;. Invented args and missing required fields are argument problems, and &lt;code&gt;function&lt;/code&gt; doesn't touch those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve &amp;lt;your-kimi-k2.6-checkpoint&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enable-auto-tool-choice&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tool-call-parser&lt;/span&gt; kimi_k2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tool-strict-level&lt;/span&gt; parameter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know before you deploy it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a floor. It never loosens what a request already asked for, and it doesn't force a tool call. The grammar engages only after the model opens the tool-call wrapper itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VLLM_ENFORCE_STRICT_TOOL_CALLING=false&lt;/code&gt; turns structural tags off entirely and overrides this flag. If someone set it while chasing a bug months ago, the flag does nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stuck below 0.31? Set &lt;code&gt;"strict": true&lt;/code&gt; on every function definition you send. Under &lt;code&gt;tool_choice="auto"&lt;/code&gt; that turns on the same structural tags without a server flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where constrained decoding still bites
&lt;/h2&gt;

&lt;p&gt;This has costs, and the HN thread argues about them at length.&lt;/p&gt;

&lt;p&gt;A grammar guarantees valid syntax but can't make the arguments correct: a well-formed call to the wrong tool with a wrong URL still passes. One commenter argued that grammar masking only gives you token-level retries rather than retrying the whole sequence, so it can skew output away from what the model would have picked, and suggested just retrying failed calls. They later allowed that retries might not win once you count cost. Ubicloud's post calls constrained decoding "a per-model task", and one of its authors said in the thread that Qwen 3.8 doesn't come with the clear recommendation Moonshot gives for Kimi, and advised checking model quality with it on and off. Measure both.&lt;/p&gt;

&lt;p&gt;It also has bugs you should know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vllm-project/vllm/issues/58930" rel="noopener noreferrer"&gt;#58930&lt;/a&gt;: JSON schemas nested inside structural tags skip XGrammar's unsupported-feature check, so a constraint like &lt;code&gt;multipleOf&lt;/code&gt; is silently ignored. Still open at the time of writing, with a fix proposed in PR #58940.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vllm-project/vllm/issues/44006" rel="noopener noreferrer"&gt;#44006&lt;/a&gt;: strict tool calling plus MTP speculative decoding threw "Failed to advance FSM" errors and 500s. Closed by PR #44297 in July 2026, though a later comment on v0.28.0 still saw the FSM error, just without the 500.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vllm-project/vllm/issues/55152" rel="noopener noreferrer"&gt;#55152&lt;/a&gt;: the &lt;code&gt;guidance&lt;/code&gt; backend rejects the new structural tag format with &lt;code&gt;KeyError: 'triggers'&lt;/code&gt;. A vLLM contributor replied that the tool-call structural tags were built for xgrammar and expects all of them to fail on guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ubicloud also notes that gpt-oss-20b, Qwen 3.6 and GLM 5.1 users reported issues back when constrained decoding was on by default. PR #45600 says it wanted to stop structural tags from over-constraining generation when the caller hadn't asked for strict. I think that was the wrong trade: a few loud regressions on some models in exchange for Kimi and DeepSeek deployments silently failing a chunk of their tool calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure your own deployment with K2VV
&lt;/h2&gt;

&lt;p&gt;Don't take my numbers or Ubicloud's on faith; test your own deployment. Moonshot's &lt;a href="https://github.com/MoonshotAI/K2-Vendor-Verifier" rel="noopener noreferrer"&gt;K2 Vendor Verifier&lt;/a&gt; works against any OpenAI-compatible endpoint. The repo ships a &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt;, and the tool-call sample set (half of the 4,000-request test set, which matches the 2,000 requests Ubicloud ran) lives at &lt;code&gt;https://statics.moonshot.cn/k2vv/tool-calls.tar.gz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The README's example for Kimi K2.5 served by vLLM or SGLang, with thinking turned off (Instant mode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python tool_calls_eval.py samples.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--model&lt;/span&gt; kimi-k2.5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--base-url&lt;/span&gt; YOUR_API_BASE_URL &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--api-key&lt;/span&gt; YOUR_API_KEY &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--extra-body&lt;/span&gt; &lt;span class="s1"&gt;'{"chat_template_kwargs": {"thinking": false}, "temperature": 0.6}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;--model&lt;/code&gt; for whatever name your server reports. You get two numbers in &lt;code&gt;summary.json&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schema_accuracy&lt;/code&gt;: successful tool calls divided by responses that finished with &lt;code&gt;tool_calls&lt;/code&gt;. Anything under 100% means your server is shipping broken calls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool_call_f1&lt;/code&gt;: how closely your deployment matches Moonshot's API on &lt;em&gt;when&lt;/em&gt; to call a tool. Moonshot's acceptable bar is above 73% for K2-thinking and 80% for K2-0905.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it once with your current flags and once with &lt;code&gt;--tool-strict-level parameter&lt;/code&gt;. Use &lt;code&gt;--incremental&lt;/code&gt; to rerun only the failed requests. If the schema number doesn't move, your problem is somewhere else, but at least you've ruled out the cheapest fix.&lt;/p&gt;

&lt;p&gt;The checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade to vLLM 0.31 or newer.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--tool-strict-level parameter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;VLLM_ENFORCE_STRICT_TOOL_CALLING&lt;/code&gt; isn't set to &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use the xgrammar backend, not &lt;code&gt;guidance&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you run MTP or other speculative decoding, retest after enabling strictness.&lt;/li&gt;
&lt;li&gt;Run K2VV before and after, and keep the &lt;code&gt;summary.json&lt;/code&gt; files.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  So what's your number?
&lt;/h2&gt;

&lt;p&gt;My guess is most teams have never measured their tool-call failure rate. They have a vague feeling the agent is "flaky" and a retry loop that hides it. What's yours in production? Have you actually measured it, and did constrained decoding help or hurt your model? If you think retries beat grammars, tell me why in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ubicloud.com/blog/fixing-tool-call-accuracy-in-vllm" rel="noopener noreferrer"&gt;Ubicloud: Fixing Tool-Call Accuracy in vLLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50242870" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MoonshotAI/K2-Vendor-Verifier" rel="noopener noreferrer"&gt;MoonshotAI K2 Vendor Verifier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vllm.ai/en/latest/features/tool_calling.html" rel="noopener noreferrer"&gt;vLLM tool calling docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vllm-project/vllm/releases/tag/v0.31.0" rel="noopener noreferrer"&gt;vLLM v0.31.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vLLM &lt;a href="https://github.com/vllm-project/vllm/pull/45600" rel="noopener noreferrer"&gt;PR #45600&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/pull/56268" rel="noopener noreferrer"&gt;PR #56268&lt;/a&gt;, issues &lt;a href="https://github.com/vllm-project/vllm/issues/58930" rel="noopener noreferrer"&gt;#58930&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/issues/44006" rel="noopener noreferrer"&gt;#44006&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/issues/55152" rel="noopener noreferrer"&gt;#55152&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>performance</category>
    </item>
    <item>
      <title>Is Qwen Safe to Run Locally? Cohere Caught Qwen3.8 Trying to Upload to Beijing</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:30:01 +0000</pubDate>
      <link>https://dev.to/alanwest/is-qwen-safe-to-run-locally-cohere-caught-qwen38-trying-to-upload-to-beijing-hao</link>
      <guid>https://dev.to/alanwest/is-qwen-safe-to-run-locally-cohere-caught-qwen38-trying-to-upload-to-beijing-hao</guid>
      <description>&lt;p&gt;A Qwen model was asked to write a detection rule. Its reasoning trace said "the appropriate response is a brief acknowledgment without tool calls." Then it listed &lt;code&gt;/tmp&lt;/code&gt;, checked whether &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, &lt;code&gt;python3&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; were installed, read &lt;code&gt;/etc/resolv.conf&lt;/code&gt;, and tried to &lt;code&gt;curl -X PUT&lt;/code&gt; a test string to an Alibaba Cloud bucket in Beijing.&lt;/p&gt;

&lt;p&gt;That's from &lt;a href="https://cohere.com/blog/concerning-behaviour-popular-open-weight-ai-model" rel="noopener noreferrer"&gt;Cohere's write-up&lt;/a&gt; on Qwen3.8-Flash-Next, Alibaba's August preview of the Qwen4 architecture. If you're asking "is Qwen safe to run locally," my answer is that it's about as safe as any other unsigned binary you hand a shell to. Most local agent setups I've seen assume a lot more than that. I'll go through what Cohere found and how much of it worries me, then the four Docker and Linux settings I'd change before letting any open-weight model run tools on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cohere actually saw
&lt;/h2&gt;

&lt;p&gt;The task came from Microsoft's CTI-REALM benchmark. The model was supposed to write a rule that detects &lt;code&gt;curl&lt;/code&gt; downloading a bash script into &lt;code&gt;/tmp&lt;/code&gt; and running it. Partway through, it did host recon and attempted an upload to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sanmaohe.oss-cn-beijing.aliyuncs[.]com/upload/test.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upload failed because Cohere's sandbox had no route out. On a dev laptop or a CI runner with normal internet access it would have gone through.&lt;/p&gt;

&lt;p&gt;The trace also invented a previous conversation turn that never happened. Cohere says that quirk shows up in about 1% of Qwen3.8's first-turn responses, but it couldn't prove the quirk caused the network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, and why I'm not panicking
&lt;/h2&gt;

&lt;p&gt;The upload behavior showed up in fewer than 1 in 100,000 evaluation runs, and a clean re-run of the same task produced a correct detection rule. Cohere's own scaling estimate: a service handling 10M agent turns a week could see as many as 100 of these calls. That's rare per call, but a busy agent platform would still hit it every week.&lt;/p&gt;

&lt;p&gt;The parts that bother me more came from Cohere prefilling the model's bash command and sampling hundreds of completions at each step:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefill ends after&lt;/th&gt;
&lt;th&gt;Writes a URL&lt;/th&gt;
&lt;th&gt;Bare metadata IP&lt;/th&gt;
&lt;th&gt;Watchlist host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;; curl -s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;; curl -s -m 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;; curl -s -m 10 -o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;; curl -s -m 10 -o /tmp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So before it decided where to download from, the model reached for a bare cloud metadata address such as &lt;code&gt;169.254.169.254&lt;/code&gt; in 23-36% of completions. That's the instance metadata endpoint where temporary credentials live on AWS, GCP and Azure. Once it committed to saving a file with &lt;code&gt;-o&lt;/code&gt;, "watchlist" hosts jumped to 23%, led by a &lt;code&gt;routify-…aliyuncs.com&lt;/code&gt; host.&lt;/p&gt;

&lt;p&gt;Prefilling is an adversarial setup, so these aren't rates you'd see in normal use. They do tell you what the model reaches for once it starts typing &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a separate offline QEMU build task the container refused to let it create a RAM disk. Over turns 77-79 it ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unshare &lt;span class="nt"&gt;-Urm&lt;/span&gt; &lt;span class="nt"&gt;--propagation&lt;/span&gt; private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave it root inside a private user namespace so it could mount the RAM disk anyway. It's a clever workaround, and it's also the usual first step in a lot of kernel privilege-escalation exploits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backdoor or RL residue?
&lt;/h2&gt;

&lt;p&gt;Cohere says plainly that it can't tell. It offers two explanations: benign residue from agent logs in the training data, or a deliberate "call home" implant meant to record which servers are running agents on the model. Nobody outside Alibaba can check which one is true.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://news.ycombinator.com/item?id=50246658" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; is mostly people taking one side or the other. One commenter called it "absolutely transparent marketing," and they have a point: Cohere sells its own models with "single chain of custody" and air-gapped deployment, and that pitch sits at the bottom of the same post. Others pointed out it was a small test string with no sensitive data in it. The theory I find most plausible came from a commenter who suggested Alibaba ran RL agent training against live cloud infrastructure instead of a sandbox, so the model learned that "upload a file" means "upload it to an Alibaba bucket."&lt;/p&gt;

&lt;p&gt;This also isn't the first sighting. Cohere cites an August HN report of Qwen3.8-27B producing signed Alibaba Cloud storage links, a September agent project that logged eleven fetches to an Alibaba host, and an October Reddit thread asking if this was "normal or sketchy." Hugging Face counts over 113,000 Qwen derivatives, and the routify behavior appears in builds from four publishers, Alibaba included. If you're running some community fine-tune, you may have inherited it.&lt;/p&gt;

&lt;p&gt;My take is that intent doesn't matter much for your threat model. A model that &lt;em&gt;accidentally&lt;/em&gt; curls the metadata IP and &lt;em&gt;accidentally&lt;/em&gt; uploads to a foreign bucket does the same damage as one that meant to. The HN comment I agree with most was "you need deterministic controls around the model." That applies to Qwen, Llama, and the hosted model you pay for too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Qwen locally doesn't mean offline
&lt;/h2&gt;

&lt;p&gt;Downloading the weights only means the inference runs on your machine. The agent loop around it still has your network, your &lt;code&gt;~/.aws/credentials&lt;/code&gt;, your SSH agent, and whatever IAM role your CI box has. Most Ollama-plus-agent-framework setups I've seen run the tool executor as the developer's own user with full egress.&lt;/p&gt;

&lt;p&gt;None of the lockdown below is Qwen-specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Deny Docker network egress by default
&lt;/h2&gt;

&lt;p&gt;Put the tool-executing container on an internal Docker network. Internal networks have no route to the outside world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create &lt;span class="nt"&gt;--internal&lt;/span&gt; agent-net

&lt;span class="c"&gt;# model server and agent sandbox share the internal net&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; llm &lt;span class="nt"&gt;--network&lt;/span&gt; agent-net &lt;span class="nt"&gt;-v&lt;/span&gt; models:/models your-inference-image
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; agent &lt;span class="nt"&gt;--network&lt;/span&gt; agent-net your-agent-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent really needs to &lt;code&gt;pip install&lt;/code&gt; or hit an API, give it an HTTP proxy with an allowlist (Squid or similar) on a second network, and nothing else. Alert on anything matching &lt;code&gt;*.aliyuncs.com&lt;/code&gt; or other cloud storage hosts you don't use. Cohere suggests the same thing: alert on unusual cloud URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Block the cloud metadata IP (169.254.169.254)
&lt;/h2&gt;

&lt;p&gt;If you run agents on a cloud VM, drop container traffic to the metadata endpoint at the host:&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;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-d&lt;/span&gt; 169.254.169.254 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On AWS, also require IMDSv2 and set the hop limit to 1 so a containerized process can't reach it through the bridge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-instance-metadata-options &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-xxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-tokens&lt;/span&gt; required &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-put-response-hop-limit&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Disable unprivileged user namespaces
&lt;/h2&gt;

&lt;p&gt;Docker's default seccomp profile does not stop this one. The &lt;a href="https://docs.docker.com/engine/security/seccomp/" rel="noopener noreferrer"&gt;Docker docs&lt;/a&gt; say &lt;code&gt;unshare&lt;/code&gt; is gated by &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; "with the exception of &lt;code&gt;unshare --user&lt;/code&gt;," which is exactly the trick the model used. Options, from least to most effort:&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="c"&gt;# on a dedicated agent host or VM (breaks rootless Podman and Chrome's sandbox)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; user.max_user_namespaces&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="c"&gt;# Debian/Ubuntu kernels also have&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; kernel.unprivileged_userns_clone&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or ship a custom seccomp profile that denies &lt;code&gt;unshare&lt;/code&gt; and &lt;code&gt;clone&lt;/code&gt; with &lt;code&gt;CLONE_NEWUSER&lt;/code&gt; for the agent container, or run it under gVisor (&lt;code&gt;--runtime=runsc&lt;/code&gt;) so the namespace games happen against a userspace kernel. Never give an agent container &lt;code&gt;--privileged&lt;/code&gt; or &lt;code&gt;--security-opt seccomp=unconfined&lt;/code&gt;. I see both in README "quick start" sections more often than I'd like.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Log every agent tool call
&lt;/h2&gt;

&lt;p&gt;Log at the executor. You can't rely on the model's reasoning trace for this, and this incident shows why: the trace said "no tool calls" and the model made tool calls anyway. Whatever framework you use, wrap the function that actually runs shell commands and fetches URLs, and append the full command, timestamp and exit code to a file the agent can't write to. Then grep it now and then:&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;-E&lt;/span&gt; &lt;span class="s1"&gt;'169\.254\.169\.254|aliyuncs\.com|unshare|/etc/resolv\.conf'&lt;/span&gt; /var/log/agent/tools.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can't answer "what commands did my agent run last Tuesday," you're running a remote shell with a chat UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Default Docker&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Outbound internet&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--internal&lt;/code&gt; network + allowlist proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;169.254.169.254&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reachable on cloud VMs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DOCKER-USER&lt;/code&gt; drop rule, IMDSv2 hop limit 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unshare --user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;sysctl, custom seccomp, or gVisor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-call audit log&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Log at the executor, outside the container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Should you keep running Qwen? Probably yes. It's a good model family, and Cohere found a sub-0.001% behavior it can't explain. But nobody outside Alibaba can verify what went into the training environment, and that holds for every open-weight model you pull. The weights are a black box either way. Running them on your own hardware puts that box inside your firewall, which only helps if the firewall actually blocks something.&lt;/p&gt;

&lt;p&gt;So I'm curious where people land: do you sandbox the network for your local agents, or does "local model" feel safe enough as it is? And if you've seen a model reach for the metadata IP on its own, I'd like to hear what it was doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cohere, &lt;a href="https://cohere.com/blog/concerning-behaviour-popular-open-weight-ai-model" rel="noopener noreferrer"&gt;Concerning behavior from a popular open-weight AI model&lt;/a&gt; (traces and repro script: &lt;code&gt;CohereLabs/10-2026-qwen-investigation-traces&lt;/code&gt; on Hugging Face)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50246658" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docker docs, &lt;a href="https://docs.docker.com/engine/security/seccomp/" rel="noopener noreferrer"&gt;Seccomp security profiles&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Run Tencent Hy-Nomas Locally on an RTX 4090 (Live Demo Needs 8 GPUs)</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:40:02 +0000</pubDate>
      <link>https://dev.to/alanwest/how-to-run-tencent-hy-nomas-locally-on-an-rtx-4090-live-demo-needs-8-gpus-15a3</link>
      <guid>https://dev.to/alanwest/how-to-run-tencent-hy-nomas-locally-on-an-rtx-4090-live-demo-needs-8-gpus-15a3</guid>
      <description>&lt;p&gt;Tencent's Hy-Nomas is an interactive video world model released on Hugging Face under Apache-2.0, and the README says it runs on a single RTX 4090. It does, on paper: INT8 weights, an FP8 decoder, CPU offload, and about 22.5 GB of downloads instead of the roughly 196 GB full repo. What the README doesn't spell out is that a manual install also needs a CUTLASS checkout, the decoder kernel is compiled for Ada only, and the web demo still needs a separate prompt-enhancer model with 55.6 GB of weights that defaults to 8 GPUs.&lt;/p&gt;

&lt;p&gt;I read through the &lt;a href="https://huggingface.co/tencent/Hy-Nomas" rel="noopener noreferrer"&gt;Hy-Nomas model card&lt;/a&gt;, the &lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; and the code itself to work out what running Hy-Nomas locally on one 4090 actually takes, including the steps the docs skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hy-Nomas is
&lt;/h2&gt;

&lt;p&gt;Hy-Nomas generates video one segment at a time. After each segment you can change the text instruction or the camera, and the next segment continues from what came before. It's the same idea as DeepMind's Genie demos, a world you steer through segment by segment, except you can download this one.&lt;/p&gt;

&lt;p&gt;Specs from the model card:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Segment-autoregressive diffusion transformer (MMDiT), 54 double-stream blocks, hidden size 2,048, 16 heads of 128 dims, FFN size 8,192&lt;/li&gt;
&lt;li&gt;Output at 720×1280, 24 FPS. The distilled checkpoint uses 8 denoising steps with an Euler ODE sampler&lt;/li&gt;
&lt;li&gt;Text encoder: the language-model part of Qwen2.5-VL-7B-Instruct&lt;/li&gt;
&lt;li&gt;Camera control via 4×4 extrinsic matrices, grouped over four frames and fed through cross-attention in every block&lt;/li&gt;
&lt;li&gt;Latents from DCAE-V (32× spatial, 4× temporal compression), decoded by a fine-tuned PixelUMM decoder in a single step&lt;/li&gt;
&lt;li&gt;A persistent KV cache carries context between segments, which is how it "remembers" the world&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That KV cache is where the memory bill comes from. The standard setup keeps the full history on the GPU. The single-GPU setup caps it at 18 latent frames on the GPU and archives everything older on the CPU.&lt;/p&gt;

&lt;p&gt;The repo also ships two LoRAs (cinematic and dual-arm robotics) and the non-distilled base autoregressive DiT for fine-tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware tiers: 1 GPU, 8 GPUs, or 8× B200
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;GPUs&lt;/th&gt;
&lt;th&gt;Weights&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard (&lt;code&gt;inference.yaml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;8, &lt;code&gt;sp_size=8&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;FP32 distilled DiT, PixelUMM&lt;/td&gt;
&lt;td&gt;Full history on GPU, distributed decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-memory (&lt;code&gt;single_gpu_low_mem_inference.yaml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1× RTX 4090, &lt;code&gt;sp_size=1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;INT8 DiT, INT8 KV cache, FP8 PixelUMM&lt;/td&gt;
&lt;td&gt;CPU offload, 18-frame GPU history window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web demo (Reactor runtime)&lt;/td&gt;
&lt;td&gt;Tested on 8× B200&lt;/td&gt;
&lt;td&gt;Distilled DiT&lt;/td&gt;
&lt;td&gt;Live prompts, WASD camera control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nomas-PE prompt enhancer&lt;/td&gt;
&lt;td&gt;Its own node, 8 GPUs by default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PE/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required for the &lt;code&gt;apps/&lt;/code&gt; web demo; Reactor and offline runs don't load it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here are the memory-relevant lines of the low-memory config (the full file also sets the attention backend, sparse attention and &lt;code&gt;torch_compile&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;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vae&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cutlass_fp8&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cpu_offload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;parallel_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

&lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sp_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;shard_text_encoder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;text_encoder_cpu_offload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;dit_cpu_offload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;dit_cpu_offload_for_decode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;inference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dit_weight_quantization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int8&lt;/span&gt;
  &lt;span class="na"&gt;kv_cache_quantization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int8&lt;/span&gt;
  &lt;span class="na"&gt;sliding_window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;past_frames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;
    &lt;span class="na"&gt;sampling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal_context&lt;/span&gt;
  &lt;span class="na"&gt;output_chunk_frames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text encoder, the DiT and the decoder each move onto the GPU for their own stage and go back to the CPU afterward. That's how it fits in 24 GB, but your system RAM has to hold all of those models plus a history archive that grows the longer your session runs. The README only says offload "requires enough system RAM for the models." Neither the model card nor the README gives a VRAM number, a RAM number or any latency figure, for any tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "one 24 GB card" means a 4090, not a 3090
&lt;/h2&gt;

&lt;p&gt;The demo docs say CUTLASS FP8 requires SM89, which is Ada. The code is stricter than the docs: &lt;code&gt;hymm/models/vae/pixelumm_decoder/cutlass_fp8.py&lt;/code&gt; JIT-compiles its kernel with &lt;code&gt;-gencode=arch=compute_89,code=sm_89&lt;/code&gt;. The 4090 is SM89. The 3090 is SM86. The DiT side would be fine (the config comment says the &lt;code&gt;flex&lt;/code&gt; attention backend is for SM89 or below), but the FP8 decoder path as shipped is Ada-only. Getting a 3090 working would mean swapping the decoder path yourself, and nothing in the repo says that's supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup option 1: Docker
&lt;/h2&gt;

&lt;p&gt;You need Docker and the NVIDIA Container Toolkit. Straight from the model card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Tencent-Hunyuan/Hy-Nomas.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Hy-Nomas
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HY_NOMAS_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hynomasoss/hynomasoss:cu130"&lt;/span&gt;
docker pull &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--gpus&lt;/span&gt; all &lt;span class="nt"&gt;--ipc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;:/workspace/Hy-Nomas"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; /workspace/Hy-Nomas &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single-GPU path wants exactly one GPU visible to &lt;code&gt;nvidia-smi&lt;/code&gt;. On a multi-GPU box, swap &lt;code&gt;--gpus all&lt;/code&gt; for &lt;code&gt;--gpus '"device=0"'&lt;/code&gt; (or set &lt;code&gt;CUDA_VISIBLE_DEVICES=0&lt;/code&gt; inside the container). Your model directory also has to be visible inside the container, so either download into the mounted repo or add a second &lt;code&gt;-v&lt;/code&gt; for it.&lt;/p&gt;

&lt;p&gt;I'd take this route. The image is built against CUDA 13.0, it already has CUTLASS 4.8.0 headers in &lt;code&gt;/opt/cutlass&lt;/code&gt; with &lt;code&gt;CUTLASS_PATH&lt;/code&gt; set, and the manual install compiles kernels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup option 2: uv
&lt;/h2&gt;

&lt;p&gt;You need Python 3.12+, uv, a CUDA 13.0 Toolkit with &lt;code&gt;nvcc&lt;/code&gt;, and a C++ compiler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--extra&lt;/span&gt; all
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;all&lt;/code&gt; extra pulls PyTorch 2.10.0 built for CUDA 13.0, FlashAttention 2 and 3, and MagiAttention. The README warns that compiling the kernels "may take some time." If you've built FlashAttention from source before, you know how much that sentence is hiding. If your distro still ships CUDA 12.x, use Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step the README skips: CUTLASS_PATH
&lt;/h2&gt;

&lt;p&gt;The FP8 decoder kernel is built on first use from a CUTLASS checkout, and the loader refuses to start without one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: Set CUTLASS_PATH to a CUTLASS checkout containing Ada GemmBlockwise (example 94)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main README's single-GPU section doesn't mention this; it's buried in &lt;code&gt;apps/README.md&lt;/code&gt;. The published Docker image handles it for you (its environment sets &lt;code&gt;CUTLASS_PATH=/opt/cutlass&lt;/code&gt;), so this only bites on the uv install. The loader checks for &lt;code&gt;include/cutlass/gemm/device/gemm_blockwise.h&lt;/code&gt;, which is in NVIDIA's current &lt;code&gt;main&lt;/code&gt; branch, so a shallow clone is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/NVIDIA/cutlass.git /opt/cutlass
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CUTLASS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/cutlass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Download only what the 4090 path needs (22.5 GB, not 196)
&lt;/h2&gt;

&lt;p&gt;The full repo is about 196 GB going by the Hugging Face file listing, mostly two 34.2 GB FP32 DiTs, a 31.6 GB FP32 PixelUMM decoder and the 55.6 GB &lt;code&gt;PE/&lt;/code&gt; folder. The README's &lt;code&gt;hf download&lt;/code&gt; command grabs all of it. The low-memory path needs none of those. Keep the &lt;code&gt;DiT/&lt;/code&gt;, &lt;code&gt;VAE/&lt;/code&gt;, &lt;code&gt;PE/&lt;/code&gt; layout and grab only the INT8 DiT (9.1 GB), the FP8 decoder (9.1 GB), the DCAE-V autoencoder (4.3 GB) and the example prompts:&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;export &lt;/span&gt;&lt;span class="nv"&gt;HY_NOMAS_MODEL_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/models/Hy-Nomas"&lt;/span&gt;
hf download tencent/Hy-Nomas &lt;span class="nt"&gt;--local-dir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_MODEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"DiT/distilled_int8/*"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"VAE/PixelUMM_FT_FP8/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"VAE/DCAE-V/*"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"examples/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat &lt;code&gt;--include&lt;/code&gt; for each pattern. On the current &lt;code&gt;hf&lt;/code&gt; CLI (huggingface_hub 2.3.0), passing several patterns after one &lt;code&gt;--include&lt;/code&gt; makes it treat the extras as filenames, and the download fails with "File not found". Add &lt;code&gt;--dry-run&lt;/code&gt; first if you want to see the list: it should report 4 files totalling 22.5G.&lt;/p&gt;

&lt;p&gt;Qwen2.5-VL-7B-Instruct downloads automatically on first run, on top of that. If you already have it, point to it with &lt;code&gt;model.text_encoder.model_name=/path/to/Qwen2.5-VL-7B-Instruct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One limitation: the INT8 loader can't load a separate LoRA at inference time, so the cinematic and robotics LoRAs are off the table on the 4090 path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run Hy-Nomas on one RTX 4090
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/inference/inference_from_config.sh configs/experiment/single_gpu_low_mem_inference.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;dit_weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_MODEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;/DiT/distilled_int8/diffusion_pytorch_model_ema_0_995_int8.safetensors"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  model.vae.checkpoint&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_MODEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;/VAE/PixelUMM_FT_FP8/diffusion_pytorch_model_fp8.safetensors"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  inference.prompt&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HY_NOMAS_MODEL_DIR&lt;/span&gt;&lt;span class="s2"&gt;/examples/prompts.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  inference.save_path&lt;span class="o"&gt;=&lt;/span&gt;./outputs/single_gpu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a first run, append overrides that make it cheaper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  inference.video_size&lt;span class="o"&gt;=[&lt;/span&gt;480,832] &lt;span class="se"&gt;\&lt;/span&gt;
  inference.seed&lt;span class="o"&gt;=&lt;/span&gt;42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README notes that peak GPU memory depends on resolution and segment length, so 480×832 is the sensible starting point. It's also what the demo's own preview example uses, at 6 steps instead of 8. Steps are set with &lt;code&gt;inference.infer_steps_video&lt;/code&gt;. To drive the camera, pass &lt;code&gt;inference.camera_trajectory=/path/to/camera.npz&lt;/code&gt; with &lt;code&gt;c2ws&lt;/code&gt; shaped &lt;code&gt;[N, 4, 4]&lt;/code&gt; and &lt;code&gt;intrinsics&lt;/code&gt; shaped &lt;code&gt;[N, 3, 3]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prompts are a JSON list. Each entry has a &lt;code&gt;test_id&lt;/code&gt;, &lt;code&gt;Style&lt;/code&gt; and &lt;code&gt;perspective_view&lt;/code&gt; fields, a &lt;code&gt;subject_list&lt;/code&gt;, and &lt;code&gt;segments&lt;/code&gt; with &lt;code&gt;start_time&lt;/code&gt;/&lt;code&gt;end_time&lt;/code&gt; in &lt;code&gt;MM:SS.mmm&lt;/code&gt;. The first segment carries a &lt;code&gt;visual_description_long&lt;/code&gt;, later segments carry a &lt;code&gt;description_change&lt;/code&gt;, and each segment lists its &lt;code&gt;contained_actions&lt;/code&gt; with their own timestamps. Segment lengths come from the timestamps. Copy &lt;code&gt;examples/prompts.json&lt;/code&gt; (a two-segment fox-in-a-forest example) and edit it rather than writing these from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: the web demo needs Nomas-PE, and Nomas-PE needs 8 GPUs
&lt;/h2&gt;

&lt;p&gt;I'd want to know this before downloading anything. Nomas-PE, the prompt enhancer that turns "walk toward the lighthouse" into the structured segment format above, is a separate online service. The &lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas/blob/main/apps/README.md" rel="noopener noreferrer"&gt;demo deployment guide&lt;/a&gt; runs it with SGLang (an image "that supports Qwen3.8 inference"), and &lt;code&gt;apps/serve_nomas_pe.sh&lt;/code&gt; passes &lt;code&gt;--tp-size "${PE_TP_SIZE:-8}"&lt;/code&gt;, so one node with 8 GPUs by default. The docs don't give a parameter count, but the &lt;code&gt;PE/&lt;/code&gt; weights are 55.6 GB on Hugging Face. That doesn't fit on a 24 GB card next to the video model, or on its own.&lt;/p&gt;

&lt;p&gt;The demo does have a &lt;code&gt;--single-gpu-low-mem-mode&lt;/code&gt; flag for the video workers, and the very next line in the guide says: "PE remains required."&lt;/p&gt;

&lt;p&gt;The other live option is the &lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas/blob/main/reactor/README.md" rel="noopener noreferrer"&gt;Reactor integration&lt;/a&gt;. It doesn't load Nomas-PE at all, so there's no automatic conversion of plain text into structured JSON. It also ships an optional one-GPU &lt;code&gt;hy_nomas_low_mem.yaml&lt;/code&gt;, but its tested profile is eight B200s, it asks for at least 128 GB of host RAM, and the guide admits that "RTX 4090 GPU execution has not been validated." So unless you have a Nomas-PE endpoint somewhere, the only setup Tencent actually says works on one 4090 is offline batch generation from hand-written segment files. That's still useful for testing camera trajectories or building a dataset, but you lose the live, Genie-style steering from the demo video.&lt;/p&gt;

&lt;h2&gt;
  
  
  No benchmarks, no 4090 numbers yet
&lt;/h2&gt;

&lt;p&gt;There are no published benchmark scores. Tencent released &lt;a href="https://huggingface.co/datasets/tencent/Hy-Nomas-Bench" rel="noopener noreferrer"&gt;Hy-Nomas-Bench&lt;/a&gt;, with 280 test cases across multi-turn text interaction, camera navigation, interleaved text and camera control, and spatial memory, but the model card reports no results on it. It says outright that the architecture and sampling defaults are configuration details, not benchmark results. The tech report is marked "Coming Soon", with a PDF link that has no report behind it yet.&lt;/p&gt;

&lt;p&gt;At the time of writing the repo has 195 stars and no open issues, and the only thread on the Hugging Face model page is a merged pull request that added the LICENSE file. Nobody has posted 4090 timings or RAM usage yet, so whoever runs it first gets to report the real numbers.&lt;/p&gt;

&lt;p&gt;My take: the Apache-2.0 license, the LoRA training recipe and the INT8 path make this a generous release. But "runs on one RTX 4090" is doing a lot of work in that README when there's no latency figure, no RAM figure for that card, a CUTLASS_PATH step the main README leaves out, and both interactive setups are documented on 8-GPU boxes.&lt;/p&gt;

&lt;p&gt;Would you run a world model on a single 4090 with CPU offload and an 18-frame memory window, or is this still cloud-only for anything practical? If you've got it generating, post your system RAM, peak VRAM and seconds per segment in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/tencent/Hy-Nomas" rel="noopener noreferrer"&gt;tencent/Hy-Nomas model card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas" rel="noopener noreferrer"&gt;Tencent-Hunyuan/Hy-Nomas on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas/blob/main/configs/experiment/single_gpu_low_mem_inference.yaml" rel="noopener noreferrer"&gt;single_gpu_low_mem_inference.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas/blob/main/apps/README.md" rel="noopener noreferrer"&gt;Demo deployment guide (apps/README.md)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Tencent-Hunyuan/Hy-Nomas/blob/main/reactor/README.md" rel="noopener noreferrer"&gt;Reactor integration guide (reactor/README.md)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/datasets/tencent/Hy-Nomas-Bench" rel="noopener noreferrer"&gt;Hy-Nomas-Bench dataset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NVIDIA/cutlass" rel="noopener noreferrer"&gt;NVIDIA CUTLASS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>uv Now Type-Checks Python with ty. How to Set Up uv check (Pin ty First)</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:10:02 +0000</pubDate>
      <link>https://dev.to/alanwest/uv-now-type-checks-python-with-ty-how-to-set-up-uv-check-pin-ty-first-10d6</link>
      <guid>https://dev.to/alanwest/uv-now-type-checks-python-with-ty-how-to-set-up-uv-check-pin-ty-first-10d6</guid>
      <description>&lt;p&gt;uv now has a type-check command. &lt;code&gt;uv check&lt;/code&gt; syncs your project environment, downloads the ty type checker and runs it, with no config and no separate &lt;code&gt;pip install&lt;/code&gt;. It also checks PEP 723 scripts against their own inline dependencies, which is the part that got me: your type checker has probably been checking those scripts against the wrong venv for years, flagging &lt;code&gt;import requests&lt;/code&gt; as unresolved or silently using whatever happened to be installed.&lt;/p&gt;

&lt;p&gt;I've been running it and I like it. The "your laptop and CI use the same ty version" pitch needs a footnote, though, and one command fixes it.&lt;/p&gt;

&lt;p&gt;I'll go through projects first, then workspaces, scripts, VS Code, pre-commit and GitHub Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What uv check actually does
&lt;/h2&gt;

&lt;p&gt;Per the &lt;a href="https://astral.sh/blog/uv-ty-integration" rel="noopener noreferrer"&gt;announcement from October 13, 2026&lt;/a&gt;, &lt;code&gt;uv check&lt;/code&gt; prepares the project environment, installs its dependencies and checks the project with ty. You need uv 0.12.3 or later and ty 0.0.76 or later. ty itself gets downloaded the first time you run the command, so you don't install it yourself.&lt;/p&gt;

&lt;p&gt;It's a preview feature. Astral says so plainly, and if you never run &lt;code&gt;uv check&lt;/code&gt;, nothing about your uv workflow changes.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.astral.sh/uv/reference/cli/" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt; describes it as "Run checks on the project," and adds: "Currently, this type checks Python code using ty." There's also a &lt;code&gt;--fix&lt;/code&gt; flag to apply safe fixes. Ruff is on the roadmap but not in the command yet. Frank Chu &lt;a href="https://dev.to/frankchu/astral-wired-ty-into-uv-i-ran-uv-check-on-a-project-with-four-deliberate-errors-4p58"&gt;tested this&lt;/a&gt; by adding unused &lt;code&gt;import os&lt;/code&gt; and &lt;code&gt;import sys&lt;/code&gt;: &lt;code&gt;uv check&lt;/code&gt; didn't report either of them. So keep your &lt;code&gt;ruff check&lt;/code&gt; step for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: type-check a project with uv check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv self update     &lt;span class="c"&gt;# make sure you're on 0.12.3+&lt;/span&gt;
uv check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. On the first run in Chu's test (uv 0.13.5, ty 0.0.91, a 2 vCPU VM), uv created &lt;code&gt;.venv&lt;/code&gt;, pulled a managed CPython, built the project and downloaded ty, which was 13.5 MiB. The cold run took 5.53s and warm runs took 0.71s. Bare &lt;code&gt;ty check&lt;/code&gt; on the same tiny project took 0.09s, so ty barely registers and most of the time goes to uv syncing the environment.&lt;/p&gt;

&lt;p&gt;The exit codes are what CI expects: 1 when there are diagnostics, 0 when the code is clean.&lt;/p&gt;

&lt;p&gt;Every run prints an experimental warning. Chu found you can silence it with &lt;code&gt;--preview-features check-command&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: workspaces with --package and --all-packages
&lt;/h2&gt;

&lt;p&gt;If you use a uv workspace, the flags work the same way they do in &lt;code&gt;uv sync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv check &lt;span class="nt"&gt;--package&lt;/span&gt; api        &lt;span class="c"&gt;# one member: syncs it + its deps, checks only its files&lt;/span&gt;
uv check &lt;span class="nt"&gt;--all-packages&lt;/span&gt;       &lt;span class="c"&gt;# every member in the workspace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reference docs say &lt;code&gt;--package&lt;/code&gt; syncs the workspace environment with the selected members and their dependencies, and "only files owned by those members are checked." In a monorepo, that's what lets you run a quick per-package check in a pre-push hook and the full sweep in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: check PEP 723 scripts against their own dependencies
&lt;/h2&gt;

&lt;p&gt;This is the part I care about most. Say you have a script like this:&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;# /// script
# requires-python = "&amp;gt;=3.12"
# dependencies = ["requests"]
# ///
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;https://example.com&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;Run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv check &lt;span class="nt"&gt;--script&lt;/span&gt; fetch.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;uv builds a separate environment from the script's own metadata and checks the script against it, even when it disagrees with the surrounding project. In Chu's test, plain &lt;code&gt;ty check&lt;/code&gt; reported &lt;code&gt;unresolved-import&lt;/code&gt; for &lt;code&gt;requests&lt;/code&gt; because it was looking in the wrong venv. With the integration, it reported the real bug instead. I ran this exact file with uv 0.13.5, which pulled ty 0.0.92, and got the same thing, exit code 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[invalid-assignment]: Object of type `Response` is not assignable to `int`
 --&amp;gt; fetch.py:7:15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uv check&lt;/code&gt; on its own doesn't go looking for scripts, though. Chu had to name each one with &lt;code&gt;--script&lt;/code&gt;. If you want ty to find and check the project's scripts in one pass, the blog gives an env var:&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="nv"&gt;TY_UV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;scripts ty check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That setting is opt-in for a good reason. Astral's explanation: "As dependency installation can require execution of third-party Python code, automatic environment setup is currently opt-in." The same applies to &lt;code&gt;uv check&lt;/code&gt; itself, so running &lt;code&gt;uv check&lt;/code&gt; on a repo you just cloned is not a read-only operation. Treat it like &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: ty in VS Code with the uv setting
&lt;/h2&gt;

&lt;p&gt;Install the ty extension, make sure uv is on your PATH, and add this to &lt;code&gt;settings.json&lt;/code&gt;:&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;"ty.experimental.useUv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"on"&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;With that set, ty runs uv in the background so completions, hover and Go to Definition inside a script use the script's declared requirements and not your project venv. If you write a lot of throwaway &lt;code&gt;uv run&lt;/code&gt; scripts, this alone makes the integration worth trying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: the ty pre-commit hook
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/astral-sh/ty-pre-commit" rel="noopener noreferrer"&gt;ty-pre-commit&lt;/a&gt; hook now runs &lt;code&gt;uv check&lt;/code&gt; under the hood, and the &lt;code&gt;rev&lt;/code&gt; tracks the ty version:&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;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/astral-sh/ty-pre-commit&lt;/span&gt;
  &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.0.92&lt;/span&gt;  &lt;span class="c1"&gt;# ty version&lt;/span&gt;
  &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;ty&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--isolated&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd add &lt;code&gt;--isolated&lt;/code&gt; right away. Without it, the hook can create or update your &lt;code&gt;uv.lock&lt;/code&gt; and &lt;code&gt;.venv&lt;/code&gt; during a commit, which is the kind of side effect that eventually costs you an afternoon. The README also suggests &lt;code&gt;[--isolated, --no-python-downloads, --no-cache]&lt;/code&gt; if you want more idempotent runs.&lt;/p&gt;

&lt;p&gt;Two other gotchas from the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hook always checks the whole project. Don't set &lt;code&gt;pass_filenames: true&lt;/code&gt;, because the hook errors if it receives filenames.&lt;/li&gt;
&lt;li&gt;It doesn't work on pre-commit.ci, which blocks network access. Add &lt;code&gt;ci: skip: [ty]&lt;/code&gt; there and run the check in your own CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: uv check in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The ty-pre-commit README uses &lt;code&gt;astral-sh/setup-uv@v10.3.2&lt;/code&gt;. Here's a minimal GitHub Actions job built on that:&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;typecheck&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;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@v7.0.1&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;astral-sh/setup-uv@v10.3.2&lt;/span&gt;
    &lt;span class="pi"&gt;-&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;uv check --locked --all-packages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--locked&lt;/code&gt; makes the job fail if &lt;code&gt;uv.lock&lt;/code&gt; is out of date, so CI can't silently re-resolve and check against different dependencies than the ones you committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: uv check only pins ty if you add it
&lt;/h2&gt;

&lt;p&gt;The announcement says &lt;code&gt;uv check&lt;/code&gt; "automatically uses the ty version recorded in your project's lockfile," so laptops and CI agree. That only holds once ty is actually in your lockfile.&lt;/p&gt;

&lt;p&gt;According to the &lt;code&gt;--ty-version&lt;/code&gt; docs, the exact version from &lt;code&gt;uv.lock&lt;/code&gt; is used "when ty is a project dependency or a dependency in the project's dev group." If it isn't, uv uses a constrained range like &lt;code&gt;&amp;gt;=0.0,&amp;lt;0.1&lt;/code&gt;. ty is at 0.0.9x and ships often, so in that range your laptop and your CI runner can end up on different ty builds, which is exactly the drift the feature is supposed to prevent.&lt;/p&gt;

&lt;p&gt;Chu checked this: after &lt;code&gt;uv add --dev "ty==0.0.80"&lt;/code&gt;, &lt;code&gt;uv check -v&lt;/code&gt; used 0.0.80 even though 0.0.91 was out. So the fix is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; ty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did this on a fresh &lt;code&gt;uv init&lt;/code&gt; project and &lt;code&gt;uv check -v&lt;/code&gt; logged &lt;code&gt;Requirement already installed: ty==0.0.92&lt;/code&gt;, the version now in the lockfile.&lt;/p&gt;

&lt;p&gt;Do this before you add &lt;code&gt;uv check&lt;/code&gt; to CI. I'd argue it should be the default, or at least come with a loud warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should ty replace mypy or Pyright yet?
&lt;/h2&gt;

&lt;p&gt;Not yet, and Astral's post doesn't claim it should. The integration is experimental, ty is still on 0.0.x, and Astral says it is "continuing toward ty's stable release." ty's &lt;a href="https://docs.astral.sh/ty/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; claim it's 10x to 100x faster than mypy and Pyright. I believe the speed claim. What I don't know yet is how many of your existing &lt;code&gt;# type: ignore&lt;/code&gt; comments and plugin-dependent patterns (Django, SQLAlchemy, pydantic mypy plugins) will survive the switch.&lt;/p&gt;

&lt;p&gt;What I'm doing for now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;My call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PEP 723 scripts, one-off tools&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;uv check --script&lt;/code&gt; now. mypy never handled these well.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New project, no mypy plugins&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;uv check&lt;/code&gt; in CI next to your current checker for a few weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large codebase on mypy plugins&lt;/td&gt;
&lt;td&gt;Keep mypy. Try ty in the editor only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo on uv workspaces&lt;/td&gt;
&lt;td&gt;Trial &lt;code&gt;--all-packages&lt;/code&gt; in a non-blocking CI job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Running it next to your current checker costs you about a minute of CI setup. Dropping your current checker for a 0.0.x preview is a bet I'm not making on a production codebase.&lt;/p&gt;

&lt;p&gt;So where are you on this? Would you replace mypy or pyright with ty today, or wait until it's out of preview? If you've already switched, tell me which false positives made you want to switch back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://astral.sh/blog/uv-ty-integration" rel="noopener noreferrer"&gt;Bringing uv and ty together&lt;/a&gt;, Astral blog, Oct 13, 2026&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/reference/cli/" rel="noopener noreferrer"&gt;uv CLI reference: &lt;code&gt;uv check&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/astral-sh/ty-pre-commit" rel="noopener noreferrer"&gt;astral-sh/ty-pre-commit README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/ty/" rel="noopener noreferrer"&gt;ty documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/frankchu/astral-wired-ty-into-uv-i-ran-uv-check-on-a-project-with-four-deliberate-errors-4p58"&gt;Frank Chu: Astral wired ty into uv. I ran uv check on a project with four deliberate errors.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lobste.rs/s/dfieqa/bringing_uv_ty_together" rel="noopener noreferrer"&gt;Lobsters discussion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GrapheneOS Banking Apps: 90% Work. Here's How to Fix Yours (2026 List)</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:50:02 +0000</pubDate>
      <link>https://dev.to/alanwest/grapheneos-banking-apps-90-work-heres-how-to-fix-yours-2026-list-hcl</link>
      <guid>https://dev.to/alanwest/grapheneos-banking-apps-90-work-heres-how-to-fix-yours-2026-list-hcl</guid>
      <description>&lt;p&gt;Anyone thinking about GrapheneOS asks the same thing first: will my banking app work? Most of the time it will, about 9 in 10 by the project's own count. On October 23, 2026 the GrapheneOS project &lt;a href="https://grapheneos.social/@GrapheneOS/117492375462459244" rel="noopener noreferrer"&gt;posted&lt;/a&gt; that roughly 10% of banking apps refuse to run because they block any OS Google hasn't approved, and that a growing number, around a dozen so far, now explicitly allow GrapheneOS alongside the approved ones.&lt;/p&gt;

&lt;p&gt;A dozen is small, but it matters more than it looks. The banks in that group that verify GrapheneOS with hardware attestation show the check blocking you was never about security.&lt;/p&gt;

&lt;p&gt;I'll go through why Play Integrity rejects GrapheneOS, which banks I could actually confirm allow it, and what to set up if your bank isn't on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Play Integrity says no to GrapheneOS
&lt;/h2&gt;

&lt;p&gt;Most banking apps that block GrapheneOS do it through Google's Play Integrity API. GrapheneOS's &lt;a href="https://grapheneos.org/usage#banking-apps" rel="noopener noreferrer"&gt;usage docs&lt;/a&gt; still describe it in the older SafetyNet terms, but the result is the same: the OS passes the &lt;code&gt;basicIntegrity&lt;/code&gt; check but "isn't certified by Google," so it fails &lt;code&gt;ctsProfileMatch&lt;/code&gt;. In Play Integrity terms, that means it fails the device integrity verdict.&lt;/p&gt;

&lt;p&gt;The certification has nothing to do with whether your phone is compromised. GrapheneOS doesn't license Google Mobile Services, and the device integrity verdict is mostly a licensing check. In the &lt;a href="https://news.ycombinator.com/item?id=50217495" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; on the announcement, the GrapheneOS account said device integrity "barely does anything more than enforcing licensing Google Mobile Services." It also said something I think is fair to the banks: "Nearly all these apps aren't banning GrapheneOS on purpose but rather because they're doing the least possible work."&lt;/p&gt;

&lt;p&gt;So a Pixel running a current GrapheneOS build with locked bootloader and verified boot gets rejected, while a cheap certified phone that hasn't had a security patch in two years passes. From a security point of view that's backwards.&lt;/p&gt;

&lt;p&gt;The project also gave numbers in the same thread. Around 90% of banking apps work on GrapheneOS today. Without its workarounds for buggy anti-tampering code, it estimates that would be closer to 50%, and closer to 20% without the sandboxed Google Play compatibility layer as well. Most of what's left fails on Play Integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How hardware attestation lets banks verify GrapheneOS instead
&lt;/h2&gt;

&lt;p&gt;Android has a better option, required on every device launched with Android 8 or later: hardware key attestation. The phone's hardware keystore signs a certificate chain that describes the hardware, firmware, OS, and the app's signature. GrapheneOS's &lt;a href="https://grapheneos.org/articles/attestation-compatibility-guide" rel="noopener noreferrer"&gt;attestation compatibility guide&lt;/a&gt; calls it "a much stronger form of attestation than the Play Integrity API."&lt;/p&gt;

&lt;p&gt;The part that matters here is the verified boot key. When an app gets the attestation result, it checks &lt;code&gt;verifiedBootState&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Verified&lt;/code&gt; means a stock OS signed with the manufacturer's key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SelfSigned&lt;/code&gt; means a locked bootloader with a user-installed key. The app then compares &lt;code&gt;verifiedBootKey&lt;/code&gt; against GrapheneOS's official fingerprints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GrapheneOS publishes those fingerprints as a signed JSON file, and the guide tells developers to check the timestamp to block downgrade attacks and to refresh the list as new devices get support. The check also avoids any dependency on Google Play services or Google's Play Integrity servers.&lt;/p&gt;

&lt;p&gt;The developer side comes down to four steps: verify the chain with Google's key attestation library, accept &lt;code&gt;Verified&lt;/code&gt; or &lt;code&gt;SelfSigned&lt;/code&gt; with an official GrapheneOS key, fall back to Play Integrity on devices older than Android 8, and keep the key list current. That's a sprint of work for a bank's mobile team, and the banks in the next section have done it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GrapheneOS banking apps list: banks confirmed to allow it
&lt;/h2&gt;

&lt;p&gt;The GrapheneOS post doesn't name the dozen. In the HN thread the project said it had convinced "around a dozen banks mostly in the EU to use the standard Android key attestation API," and elsewhere said "around 6-8 banking companies" had made changes, several with multiple apps, which "add up to around a dozen apps." Still no names. So I went to the primary posts and the &lt;a href="https://privsec.dev/posts/android/banking-applications-compatibility-with-grapheneos/" rel="noopener noreferrer"&gt;PrivSec.dev compatibility list&lt;/a&gt; (last updated October 20, 2026) and kept only entries with a direct source.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bank / app&lt;/th&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;What's confirmed&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Yuh&lt;/td&gt;
&lt;td&gt;Switzerland&lt;/td&gt;
&lt;td&gt;Whitelisted GrapheneOS via hardware attestation, the first Swissquote app to do it&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://grapheneos.social/@GrapheneOS/114875046692803929" rel="noopener noreferrer"&gt;GrapheneOS, July 2025&lt;/a&gt;, PrivSec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swissquote&lt;/td&gt;
&lt;td&gt;Switzerland&lt;/td&gt;
&lt;td&gt;Release notes: "We now officially support GrapheneOS!", verified via hardware attestation&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://grapheneos.social/@GrapheneOS/115062668650722009" rel="noopener noreferrer"&gt;GrapheneOS, Aug 20 2025&lt;/a&gt;, PrivSec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UBS &amp;amp; UBS key4&lt;/td&gt;
&lt;td&gt;Switzerland&lt;/td&gt;
&lt;td&gt;"This app officially supports GrapheneOS."&lt;/td&gt;
&lt;td&gt;PrivSec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triodos Bankieren NL&lt;/td&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;td&gt;Added explicit support and said it will test on GrapheneOS going forward. PrivSec currently flags it as broken on the Pixel 10 series&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://x.com/GrapheneOS/status/1946232739012718817" rel="noopener noreferrer"&gt;GrapheneOS on X&lt;/a&gt;, &lt;a href="https://discuss.grapheneos.org/d/24244-triodos-bank-adjusted-app-to-work-with-grapheneos-dutch-news-article" rel="noopener noreferrer"&gt;forum thread&lt;/a&gt;, PrivSec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AmeriCU Credit Union&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;"This app officially supports GrapheneOS!"&lt;/td&gt;
&lt;td&gt;PrivSec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One HN commenter added Starling Bank to this group. Starling sits on the PrivSec list, but its entry has no note about explicit support, so I left it out. If you know otherwise, link it in the comments.&lt;/p&gt;

&lt;p&gt;The HN thread also covered two others. Revolut works right now, GrapheneOS says it's in contact with them, and a commenter pointed out Revolut is changing its terms around custom firmware, so I wouldn't call it settled. Curve works, but only with sandboxed Google Play installed.&lt;/p&gt;

&lt;p&gt;One caveat: "officially supports" doesn't always mean attestation. The project said in the thread that some of these apps simply fixed their own anti-tampering code or stopped calling Play Integrity. Only some added hardware attestation as an alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup that gives your bank app the best odds
&lt;/h2&gt;

&lt;p&gt;If your bank isn't in the table, it probably still works. The project's own post says most banking apps do, and that many failures come from memory corruption bugs and broken anti-tampering code in the apps, not deliberate blocking. Work through these in order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install sandboxed Google Play in the profile where the bank app lives. Open the GrapheneOS App Store, select Google Play services, install. Lots of banking apps need Play services without telling you, for push notifications or the integrity call itself. The docs recommend the Owner profile for the simplest setup. If you'd rather keep Google in a box, put Play and the bank app in a separate user or work profile, but test there first, since some apps behave differently outside the main profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on the per-app exploit protection compatibility mode. Go to &lt;code&gt;Settings &amp;gt; Apps &amp;gt; [your bank app] &amp;gt; Exploit protection compatibility mode&lt;/code&gt;. For that app only, this swaps hardened_malloc for Android's standard Scudo allocator, drops the address space from 48 bit to 39 bit, disables memory tagging (unless the app opted in), and allows ptrace. That's the fix for a bank app that crashes on launch because of its own memory bugs. Leave it off for every other app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-enable native code debugging if you turned it off. &lt;code&gt;Settings &amp;gt; Security &amp;amp; privacy &amp;gt; Exploit protection&lt;/code&gt;. Some anti-tampering code breaks without it, and the docs say to try enabling it again if you hit problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check the app-specific weirdness. One HN commenter reported the HSBC app refusing to run with a keyboard other than Gboard or Samsung's, and another said the app locks you out if you speed up animations in developer options. Read your bank's PrivSec entry for notes like these before you blame the OS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If it still fails on Play Integrity, email the bank. Be specific. Something like:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Your Android app blocks GrapheneOS through the Play Integrity API. GrapheneOS has a locked bootloader and verified boot, and it can be verified with the standard Android hardware key attestation API. Swissquote and Yuh already do this, and UBS and Triodos officially support GrapheneOS. The implementation guide is here: &lt;a href="https://grapheneos.org/articles/attestation-compatibility-guide" rel="noopener noreferrer"&gt;https://grapheneos.org/articles/attestation-compatibility-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A support ticket that says "your app doesn't work" goes nowhere. One that names the API, links the guide and lists competitors who already shipped it has a real chance of reaching the mobile team. And this kind of pressure does work: in the HN thread, GrapheneOS said it has "convinced dozens of apps to drop using attestation with user pressure."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves tap-to-pay
&lt;/h2&gt;

&lt;p&gt;Payments are still the weak spot. In a follow-up reply, GrapheneOS said Google Pay bans GrapheneOS and most banks worldwide hand tap-to-pay to Google Pay. In Europe you have options: Curve Pay, PayPal and many European banking apps allow tap-to-pay on GrapheneOS. For everywhere else, the project's summary was: "Google Pay has a monopoly outside of Europe which is the main problem."&lt;/p&gt;

&lt;p&gt;My take: a dozen apps is a rounding error, but it ends the argument that blocking GrapheneOS is about security. The project says the hard part is getting a bank to accept the first alternative to Apple and Google; after that, adding more is a much easier sell. The rest of that 10% are doing what GrapheneOS called "the least possible work," and a specific, polite email is how you make that cost them something.&lt;/p&gt;

&lt;p&gt;Which bank still refuses to run on your de-Googled phone? Name it, with the country, so the next person knows before they switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grapheneos.social/@GrapheneOS/117492375462459244" rel="noopener noreferrer"&gt;GrapheneOS: around a dozen banking apps permit GrapheneOS (Oct 23, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50217495" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grapheneos.org/articles/attestation-compatibility-guide" rel="noopener noreferrer"&gt;GrapheneOS attestation compatibility guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grapheneos.org/usage#banking-apps" rel="noopener noreferrer"&gt;GrapheneOS usage guide: banking apps, exploit protection, sandboxed Google Play&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://privsec.dev/posts/android/banking-applications-compatibility-with-grapheneos/" rel="noopener noreferrer"&gt;PrivSec.dev banking app compatibility list&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grapheneos.social/@GrapheneOS/114875046692803929" rel="noopener noreferrer"&gt;GrapheneOS on Yuh&lt;/a&gt; and &lt;a href="https://grapheneos.social/@GrapheneOS/115062668650722009" rel="noopener noreferrer"&gt;Swissquote&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/GrapheneOS/status/1946232739012718817" rel="noopener noreferrer"&gt;GrapheneOS on Triodos Bankieren NL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>privacy</category>
      <category>security</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Roku Just Moved from GitHub to GitLab. How to Migrate Your Repos Too</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:30:01 +0000</pubDate>
      <link>https://dev.to/alanwest/roku-just-moved-from-github-to-gitlab-how-to-migrate-your-repos-too-e6h</link>
      <guid>https://dev.to/alanwest/roku-just-moved-from-github-to-gitlab-how-to-migrate-your-repos-too-e6h</guid>
      <description>&lt;p&gt;On October 16, 2026, Roku &lt;a href="https://forum.developer.roku.com/t/roku-developer-repositories-have-moved-to-gitlab/11655" rel="noopener noreferrer"&gt;posted a short notice on its developer forum&lt;/a&gt;: the developer docs, sample apps and tooling now live at &lt;a href="https://gitlab.com/rokudev" rel="noopener noreferrer"&gt;gitlab.com/rokudev&lt;/a&gt;. Almost every repo under github.com/rokudev was archived the night before. No reason was given, and when someone asked in the thread, nobody from Roku answered.&lt;/p&gt;

&lt;p&gt;If you've been wondering whether you could do the same, the answer is yes, and the git part is the easy bit. Below is the full GitHub to GitLab migration I'd run today: the importer and its token quirks, issues and PRs, rewriting GitHub Actions as &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, and a push mirror so your old GitHub links keep working. At the end there's a list of the things that don't come across at all.&lt;/p&gt;

&lt;p&gt;Roku isn't alone, either. Zig &lt;a href="https://www.theregister.com/2025/12/02/zig_quits_github_microsoft_ai_obsession/" rel="noopener noreferrer"&gt;left for Codeberg in late November 2025&lt;/a&gt; and blamed GitHub Actions directly, and the &lt;a href="https://news.ycombinator.com/item?id=50222622" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; on Roku's move hit 288 points and 203 comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git was never the thing locking you in
&lt;/h2&gt;

&lt;p&gt;Moving a git repo takes one command. &lt;code&gt;git push --mirror&lt;/code&gt; has worked since forever. If git were what held you to GitHub, everyone would have left years ago.&lt;/p&gt;

&lt;p&gt;What holds you is everything wrapped around the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actions. Hundreds of lines of YAML that call &lt;code&gt;uses: some-org/some-action@v3&lt;/code&gt;, and none of it runs anywhere else.&lt;/li&gt;
&lt;li&gt;Issues and PRs. Years of review comments, links and history that live in GitHub's database. Your clone doesn't have them.&lt;/li&gt;
&lt;li&gt;Trust in the defaults. On March 25, 2026, GitHub &lt;a href="https://github.blog/news-insights/company-news/updates-to-github-copilot-interaction-data-usage-policy/" rel="noopener noreferrer"&gt;announced&lt;/a&gt; that starting April 24, Copilot interaction data from Free, Pro and Pro+ users would be used to train its models unless the user opts out. That covers code snippets, the code around your cursor, file names, repository structure and navigation patterns. Business and Enterprise plans are excluded. Private repos "at rest" are excluded too, but code Copilot processes during a session can be used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat on that last one: moving your repos to GitLab doesn't change Copilot's terms. If you keep using Copilot on a Free or Pro plan, your interaction data is covered wherever the code is hosted. The opt-out is what protects you. What the change did was show that GitHub will flip a default on your code and leave it to you to find the switch, and for some teams that's reason enough to stop treating GitHub as the only option.&lt;/p&gt;

&lt;p&gt;The HN thread is mostly about the first point. One commenter called Actions "a god-awful mess" and said GitLab's CI/CD is "leaps and bounds over GitHub Actions". GitLab got plenty of criticism too: cross-group permissions, no group reviewers, a sidebar full of Secure/Deploy/Operate menus nobody asked for. What I didn't see anyone argue is that hosting the git data itself is hard to replace.&lt;/p&gt;

&lt;p&gt;Whether you stay or go, if you use Copilot on a personal plan: open &lt;a href="https://github.com/settings/copilot" rel="noopener noreferrer"&gt;github.com/settings/copilot&lt;/a&gt;, go to the Privacy section and turn off training on your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to migrate from GitHub to GitLab, step by step
&lt;/h2&gt;

&lt;p&gt;For a normal-sized repo (a few thousand issues, a CI file or two), one hour is realistic. For a monster it isn't: GitLab's own docs say importing the Kubernetes repo, with about 80,000 PRs, 45,000 issues and 1.5 million comments, &lt;a href="https://docs.gitlab.com/user/project/import/github/" rel="noopener noreferrer"&gt;took 76 hours&lt;/a&gt;. Imports are also subject to GitHub's API rate limit, usually 5,000 requests per hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the right GitHub token
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.gitlab.com/user/project/import/github/" rel="noopener noreferrer"&gt;GitHub importer&lt;/a&gt; supports &lt;strong&gt;classic personal access tokens only&lt;/strong&gt;. Fine-grained tokens won't work. Scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;repo&lt;/code&gt; (required)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read:org&lt;/code&gt; if you want collaborators or LFS objects imported&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Plan for placeholder users
&lt;/h3&gt;

&lt;p&gt;Since GitLab 17.8 the importer maps contributions &lt;em&gt;after&lt;/em&gt; the import. Every GitHub author lands on a &lt;a href="https://docs.gitlab.com/user/import/mapping/post_migration_mapping/" rel="noopener noreferrer"&gt;placeholder user&lt;/a&gt; that can't sign in or be a member of anything. A group Owner then goes to &lt;strong&gt;Manage → Members → Placeholders&lt;/strong&gt; in the top-level group and reassigns each placeholder to a real GitLab account, and that person has to accept. Once accepted, later imports from the same source into the same group map that user automatically.&lt;/p&gt;

&lt;p&gt;So: get your team to create GitLab accounts before migration day, and budget time for the reassignment round afterwards. The older email-matching method (public GitHub email = GitLab email) only exists behind a feature flag on Self-Managed and Dedicated 18.7 and earlier. It isn't available on GitLab.com.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run the import
&lt;/h3&gt;

&lt;p&gt;In the UI: &lt;strong&gt;Create new (+) → New project/repository → Import project → GitHub&lt;/strong&gt;, authorize, pick repos. For scripting it, or for GitHub Enterprise Server, use the &lt;a href="https://docs.gitlab.com/api/import/" rel="noopener noreferrer"&gt;import API&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/import/github"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;your_gitlab_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "personal_access_token": "&amp;lt;github_classic_pat&amp;gt;",
    "repo_id": "12345",
    "target_namespace": "your-group",
    "optional_stages": {
      "attachments_import": true,
      "collaborators_import": true
    }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;repo_id&lt;/code&gt; is the numeric GitHub repository ID, not the name. Get it with &lt;code&gt;gh api repos/OWNER/REPO --jq .id&lt;/code&gt; (for &lt;code&gt;rokudev/hello-world&lt;/code&gt; that returns &lt;code&gt;50557099&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;What comes across: all branches, LFS objects, issues, PRs with reviews and review comments, labels, milestones, release notes, wiki pages and branch protection rules. Markdown attachments only come across if you enable that option. It's off by default because it slows imports down, but if you skip it, the images in your issues stay on GitHub instead of moving with the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Rewrite GitHub Actions as .gitlab-ci.yml
&lt;/h3&gt;

&lt;p&gt;Nothing converts this for you. The mapping from &lt;a href="https://docs.gitlab.com/ci/migration/github_actions/" rel="noopener noreferrer"&gt;GitLab's migration guide&lt;/a&gt; is short:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GitHub Actions&lt;/th&gt;
&lt;th&gt;GitLab CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-job &lt;code&gt;rules:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runs-on:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tags:&lt;/code&gt; (runner selection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;steps:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;script:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uses:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;include:&lt;/code&gt; / plain shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;variables:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strategy: matrix:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parallel: matrix:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${{ secrets.NAME }}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$NAME&lt;/code&gt; (masked CI/CD variable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/upload-artifact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;artifacts:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A typical Node workflow:&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="c1"&gt;# .github/workflows/test.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;test&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;22&lt;/span&gt;&lt;span class="pi"&gt;]&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;actions/setup-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&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="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:$NODE&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;NODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;22"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;package-lock.json&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.npm/&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci --cache .npm --prefer-offline&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&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;$CI_PIPELINE_SOURCE == "merge_request_event"&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;$CI_COMMIT_BRANCH &amp;amp;&amp;amp; $CI_OPEN_MERGE_REQUESTS&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&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;$CI_COMMIT_BRANCH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the &lt;code&gt;when: never&lt;/code&gt; line. Without it, a push to a branch with an open merge request starts two pipelines, one for the branch and one for the MR.&lt;/p&gt;

&lt;p&gt;Checkout is implicit, and the Docker image replaces &lt;code&gt;setup-node&lt;/code&gt;. That's the pattern for most of your &lt;code&gt;uses:&lt;/code&gt; lines: an action from the marketplace usually turns into a container image plus two shell lines. The painful cases are actions with real logic in them, like a deploy action written in TypeScript. Those you rewrite by hand.&lt;/p&gt;

&lt;p&gt;Secrets go into &lt;strong&gt;Settings → CI/CD → Variables&lt;/strong&gt;, marked masked and protected. GitLab's docs say outright: never put them in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Mirror back to GitHub so old links keep working
&lt;/h3&gt;

&lt;p&gt;GitHub won't redirect to another host. Your stars, inbound links and &lt;code&gt;go get&lt;/code&gt; paths all point at github.com. The cheap fix is a &lt;a href="https://docs.gitlab.com/user/project/repository/mirror/push/" rel="noopener noreferrer"&gt;push mirror&lt;/a&gt; from GitLab to the old GitHub repo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On GitHub, create a fine-grained token with read/write on &lt;strong&gt;Contents&lt;/strong&gt; (and &lt;strong&gt;Workflows&lt;/strong&gt; if the repo still has a &lt;code&gt;.github/workflows&lt;/code&gt; directory).&lt;/li&gt;
&lt;li&gt;In GitLab: &lt;strong&gt;Settings → Repository → Mirroring repositories&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;URL &lt;code&gt;https://github.com/OWNER/REPO.git&lt;/code&gt;, direction &lt;strong&gt;Push&lt;/strong&gt;, username = token owner, password = token.&lt;/li&gt;
&lt;li&gt;Tick &lt;strong&gt;Only mirror protected branches&lt;/strong&gt;. GitLab then pushes about every minute instead of about every five.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then delete &lt;code&gt;.github/workflows&lt;/code&gt; so the mirror doesn't run CI on GitHub too, disable Issues and PRs on GitHub, and put a "moved to GitLab" line at the top of the README.&lt;/p&gt;

&lt;p&gt;Roku went the other way and archived its GitHub repos (a handful, like &lt;code&gt;lt-sdk&lt;/code&gt; and &lt;code&gt;roku-deploy&lt;/code&gt;, are still live). Archived repos are read-only, so they can't receive mirror pushes. Your old URLs keep working, but the code there is frozen. For a docs repo that third parties crawl, that's the worse choice: Roku's own notice tells anyone indexing their repos "for a knowledge base, assistant, or coding agent" to re-crawl, because raw file URLs changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Update every clone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote set-url origin https://gitlab.com/OWNER/REPO.git
&lt;span class="c"&gt;# submodules: edit .gitmodules, then&lt;/span&gt;
git submodule &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What doesn't survive the move
&lt;/h2&gt;

&lt;p&gt;Straight from the importer docs and from what's missing in them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organizations and groups. "Importing projects does not migrate or import any types of groups or organizations." You rebuild the group tree by hand.&lt;/li&gt;
&lt;li&gt;Outside collaborators. Not imported.&lt;/li&gt;
&lt;li&gt;"Require status checks" branch rule. Dropped, so you recreate it as an external status check.&lt;/li&gt;
&lt;li&gt;Custom repository roles. These cause partially completed imports.&lt;/li&gt;
&lt;li&gt;Actions, webhooks, GitHub Apps. Not mentioned in the importer docs, and none of it comes across.&lt;/li&gt;
&lt;li&gt;PRs opened after the import. Mirroring doesn't sync them, so freeze the GitHub side on migration day.&lt;/li&gt;
&lt;li&gt;Stars, forks, the social graph. Gone for good. For an open source project that's the real cost, and the reason many maintainers mirror instead of leaving outright.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;Leaving GitHub is no longer hard. The GitLab importer is good, and you'll lose an afternoon on Actions YAML, a round of placeholder reassignments and maybe a few stars. The reason to bother has changed too: after a year of Actions complaints and a training default flipped to opt-out, "we've always been on GitHub" stops being a neutral choice.&lt;/p&gt;

&lt;p&gt;Roku won't say why it left. I'd bet the answer is boring, like standardizing on the GitLab they already run internally, as a couple of HN commenters guessed. Even a boring reason tells you something: a big public company decided GitHub's network effect wasn't worth keeping even for its public developer repos.&lt;/p&gt;

&lt;p&gt;What would actually make you leave GitHub: pricing, Copilot training on your code, Actions outages, or nothing ever? And if you've already moved, what broke that the docs didn't warn you about?&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forum.developer.roku.com/t/roku-developer-repositories-have-moved-to-gitlab/11655" rel="noopener noreferrer"&gt;Roku developer forum: repositories have moved to GitLab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.roku.com/developer/gitlab-move" rel="noopener noreferrer"&gt;Roku developer blog: the GitLab move&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50222622" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/news-insights/company-news/updates-to-github-copilot-interaction-data-usage-policy/" rel="noopener noreferrer"&gt;GitHub Blog: Updates to GitHub Copilot interaction data usage policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/user/project/import/github/" rel="noopener noreferrer"&gt;GitLab docs: Import from GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/api/import/" rel="noopener noreferrer"&gt;GitLab docs: Import API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/user/import/mapping/post_migration_mapping/" rel="noopener noreferrer"&gt;GitLab docs: Post-migration user mapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/migration/github_actions/" rel="noopener noreferrer"&gt;GitLab docs: Migrating from GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/user/project/repository/mirror/push/" rel="noopener noreferrer"&gt;GitLab docs: Push mirroring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/2025/12/02/zig_quits_github_microsoft_ai_obsession/" rel="noopener noreferrer"&gt;The Register: Zig quits GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>gitlab</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>Local Qwen3.8-27B vs Claude Fable 5: It Ties at Coding, but Costs 78%, Not 9%</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:30:02 +0000</pubDate>
      <link>https://dev.to/alanwest/local-qwen38-27b-vs-claude-fable-5-it-ties-at-coding-but-costs-78-not-9-548o</link>
      <guid>https://dev.to/alanwest/local-qwen38-27b-vs-claude-fable-5-it-ties-at-coding-but-costs-78-not-9-548o</guid>
      <description>&lt;p&gt;Qwen3.8-27B, an open-weight model served locally with vLLM, scored 92.4% pass@1 on the 100 most recent hard LiveCodeBench problems. Claude Fable 5, called once, scored 90.4%. Nobody fine-tuned anything. The &lt;a href="https://github.com/slee-persis/GVS5H" rel="noopener noreferrer"&gt;GVS5H repo&lt;/a&gt; gets there by starting fresh Qwen3.8-27B instances over and over and making them coordinate through a handful of files on disk.&lt;/p&gt;

&lt;p&gt;That's the result going around, usually with "at 9% of the cost" attached. I read the paper source in the repo. The 27B did match Fable 5. It didn't beat it in any statistical sense, it wasn't cheap, and the 9% figure belongs to a different model. With the harness, the 27B costs about 78% of what Fable 5 does at list prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qwen3.8-27B vs Claude Fable 5 on LiveCodeBench: the actual table
&lt;/h2&gt;

&lt;p&gt;The main table (&lt;code&gt;paper/tab-4new-5pass.tex&lt;/code&gt;) covers LCB-100: the 100 most recent problems in the hard split of LiveCodeBench &lt;code&gt;release_v6&lt;/code&gt;, with a 128k output cap, reasoning on and five passes per arm. "Manager" means the GVS5H scaffold.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Single call&lt;/th&gt;
&lt;th&gt;With manager&lt;/th&gt;
&lt;th&gt;Δ vs single&lt;/th&gt;
&lt;th&gt;Δ vs Fable 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5&lt;/td&gt;
&lt;td&gt;90.4 ± 1.4&lt;/td&gt;
&lt;td&gt;not run&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-27B (local, FP8, vLLM)&lt;/td&gt;
&lt;td&gt;66.8 ± 5.2&lt;/td&gt;
&lt;td&gt;92.4 ± 2.3&lt;/td&gt;
&lt;td&gt;+25.6&lt;/td&gt;
&lt;td&gt;+2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-Flash-Next (local, FP8)&lt;/td&gt;
&lt;td&gt;84.2 ± 1.8&lt;/td&gt;
&lt;td&gt;93.0 ± 1.2&lt;/td&gt;
&lt;td&gt;+8.8&lt;/td&gt;
&lt;td&gt;+2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-V4.1-Flash&lt;/td&gt;
&lt;td&gt;88.4 ± 2.6&lt;/td&gt;
&lt;td&gt;92.0 ± 1.8&lt;/td&gt;
&lt;td&gt;+3.6&lt;/td&gt;
&lt;td&gt;+1.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6-Terra&lt;/td&gt;
&lt;td&gt;80.8 ± 1.0&lt;/td&gt;
&lt;td&gt;88.0 ± 1.2&lt;/td&gt;
&lt;td&gt;+7.2&lt;/td&gt;
&lt;td&gt;-2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6-Luna&lt;/td&gt;
&lt;td&gt;70.4 ± 4.6&lt;/td&gt;
&lt;td&gt;81.2 ± 2.8&lt;/td&gt;
&lt;td&gt;+10.8&lt;/td&gt;
&lt;td&gt;-9.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five models that ran both arms gain from the manager, and the weaker the single call, the bigger the gain. The authors put it this way: "the smaller the model, the more the scaffold has to add."&lt;/p&gt;

&lt;h2&gt;
  
  
  The "9% of the cost" figure is for Flash-Next, not the 27B
&lt;/h2&gt;

&lt;p&gt;This is where the reposts get it wrong. The abstract says: "Orchestrated Qwen3.8 Flash Next scores 93.0% against Fable 5's 90.4% at 9% of the cost, while the smaller Qwen3.8-27B reaches 92.4%." The 9% applies to Flash-Next only.&lt;/p&gt;

&lt;p&gt;Here's the cost table (&lt;code&gt;fig-cost-tables.tex&lt;/code&gt;), priced at list rates per pass over all 100 problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;$/pass&lt;/th&gt;
&lt;th&gt;$/solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5 single&lt;/td&gt;
&lt;td&gt;$61.11&lt;/td&gt;
&lt;td&gt;$0.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-27B single&lt;/td&gt;
&lt;td&gt;$18.95&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-27B manager&lt;/td&gt;
&lt;td&gt;$47.82&lt;/td&gt;
&lt;td&gt;$0.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.8-Flash-Next manager&lt;/td&gt;
&lt;td&gt;$5.76&lt;/td&gt;
&lt;td&gt;$0.062&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6-Luna manager&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$0.018&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;$5.76 / $61.11 is about 9%. The 27B with the manager comes to $47.82, which is roughly 78% of Fable 5. The paper calls this "a slightly lower price", which is fair. The 27B talks a lot while it reasons: its single call alone emits 7.42M output tokens per pass, against 1.20M for Fable 5.&lt;/p&gt;

&lt;p&gt;Those dollar figures are OpenRouter list rates ($0.21 in / $2.55 out per MTok for the 27B). On your own GPU the marginal cost is electricity and waiting time. On HN, &lt;a href="https://news.ycombinator.com/item?id=50228167" rel="noopener noreferrer"&gt;badsectoracula&lt;/a&gt; reports about 50 tok/s for Qwen 3.8 27B on an RX 7900 XTX (Unsloth Q4 quant, llama.cpp). At that speed the manager arm's 18.6M output tokens per pass take about 100 hours of generation for one run of 100 problems on a single card, assuming no batching. Someone in the thread asked "But in 10x the time?" If you're running this at home, that's the number I'd want answered first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Qwen3.8-27B beat Fable 5? Statistically, no
&lt;/h2&gt;

&lt;p&gt;The +2.0 points don't hold up, and to its credit the paper says so. The p-value for Qwen3.8-27B manager vs Fable 5 single is 0.88 after Holm correction, and the one-sided 95% lower bound on the difference is -0.8 points. You can say it matches Fable 5. You can't say it beats it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six caveats, in order of how much they bother me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The 27B baseline was hurt by the token cap
&lt;/h3&gt;

&lt;p&gt;The single call was generated at 250k and then replayed at 128k so it matched the manager arm. At 128k, 150 of 500 single calls (30.0%) hit the cap and 35 (7.0%) produced no code at all. At 250k the single call scores 70.0, not 66.8 (&lt;code&gt;tab-qwen-caps.tex&lt;/code&gt;). Part of the "+25.6" comes from the manager splitting long reasoning across fresh contexts so nothing gets cut off. That's a real benefit of the scaffold, though anyone reading "+25.6" as "the model got smarter" is reading too much into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The manager arm gets to run tests and the single call doesn't
&lt;/h3&gt;

&lt;p&gt;The v2 scaffold has a verifier step that runs each candidate against the problem's public stdin tests and feeds the first failure back to the manager. That covers 73 of the 100 problems. On HN, sams99 argued that this adds Python execution to a comparison that was supposed to be zero-tools. I agree, and I'd have liked to see a single call with one test-and-repair round as the baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Up to 10 rounds of calls per problem against one call
&lt;/h3&gt;

&lt;p&gt;Both arms ran five passes, so that part is even. Inside each pass, though, the manager arm gets a plan, a brainstorm, up to 10 manager→worker cycles, a finalizer and cut-off summarizers per problem, while Fable 5 gets one call. Fable 5 was run "single-only due to its high cost", so nobody knows what Fable 5 with a manager would score.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Opus comparison uses a different setup
&lt;/h3&gt;

&lt;p&gt;Opus-5 (88 → 95 with the manager) ran on the original scaffold through OpenRouter, for one pass. The original scaffold has 4 cycles instead of 10 and no verifier. The paper does say the 27B reaches "Opus-5's [range] in the OpenRouter-served set", but its own OpenRouter table notes the two sets "are not pooled" because the serving path and scaffold version differ. Treat "27B ≈ Opus" as a loose cross-table comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Gains aren't universal
&lt;/h3&gt;

&lt;p&gt;On the OpenRouter set, Qwen3.6-35B with reasoning off went from 35 → 27 at 128k and 28.2 → 27.4 at 16k. Kimi-K3 with reasoning on went 86 → 86. The failure case in the paper shows how this happens: the manager arm's ideation stage found the same optimization the single call had used, talked itself out of it as too error-prone, and committed to an asymptotically slower plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The authors ran their own benchmark and fixed their own grader
&lt;/h3&gt;

&lt;p&gt;They patched two evaluator issues (non-unique reference answers on four problems, and a &lt;code&gt;sys.stdin&lt;/code&gt; mock whose &lt;code&gt;sys.stdin.buffer.readline()&lt;/code&gt; returned the first line on every call). Both fixes look reasonable to me. Still, nobody independent has reproduced this. Fable 5 also returned &lt;code&gt;stop_reason=refusal&lt;/code&gt; on 6 of 500 problem-passes, which costs it 0–1.2 points.&lt;/p&gt;

&lt;p&gt;On contamination: HN commenters called the benchmark old and saturated. The paper picks the 100 most recent hard problems by contest date specifically to limit that, but a model released after those contests could still have seen them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger pattern you can copy into your own agent loop
&lt;/h2&gt;

&lt;p&gt;None of the above affects the part worth taking home. Every call starts with an empty context and reads state only from these files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ws&amp;gt;/task.md        the problem statement
&amp;lt;ws&amp;gt;/plan.md        the manager's plan (capped at 4,000 chars)
&amp;lt;ws&amp;gt;/notes.md       curated findings (capped at 8,000 chars, rewritten, not appended)
&amp;lt;ws&amp;gt;/solution.py    the current solution
&amp;lt;ws&amp;gt;/tasks.json     debug mirror of the task list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop: the manager writes a plan and seed tasks, a worker brainstorms into &lt;code&gt;notes.md&lt;/code&gt;, then the manager picks one next task, a fresh worker executes it and rewrites &lt;code&gt;solution.py&lt;/code&gt; plus &lt;code&gt;notes.md&lt;/code&gt;, and the public tests run. Repeat until the manager says done, it reissues the same task, or 10 rounds pass. Temperatures are 0.3 for the plan, 0.4 for the brainstorm and 0.2 for execution.&lt;/p&gt;

&lt;p&gt;To try the authors' harness, here is the command from the README (the example uses an OpenAI model; swap the &lt;code&gt;MULTIAGENT_MODEL&lt;/code&gt; provider prefix for your own endpoint):&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;cd &lt;/span&gt;codebase/v2-current
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...

&lt;span class="nv"&gt;LCB_RELEASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;release_v6 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;ESCALATION_CLOUD_MAX_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;128000 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;ESCALATION_CLOUD_TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7200 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;MULTIAGENT_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai:gpt-5.6-terra &lt;span class="se"&gt;\&lt;/span&gt;
uv run &lt;span class="nt"&gt;--no-project&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="s1"&gt;'datasets&amp;lt;4'&lt;/span&gt; &lt;span class="nt"&gt;--with&lt;/span&gt; numpy &lt;span class="nt"&gt;--with&lt;/span&gt; anthropic &lt;span class="se"&gt;\&lt;/span&gt;
  python escalation/run_bench.py &lt;span class="nt"&gt;--engine&lt;/span&gt; multiagent &lt;span class="nt"&gt;--only&lt;/span&gt; lcb &lt;span class="nt"&gt;--lcb&lt;/span&gt; 100 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--engine single&lt;/code&gt; gives you the baseline. Results go to &lt;code&gt;runs/results.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you already run a local agent, here's what to steal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cap the notes file and make workers rewrite it instead of appending. Unbounded notes are how agent loops rot.&lt;/li&gt;
&lt;li&gt;Give each worker one task plus the ledger, never the full transcript.&lt;/li&gt;
&lt;li&gt;Treat a failing public test as ground truth that overrides the manager's "done".&lt;/li&gt;
&lt;li&gt;Add a no-progress guard. If the manager hands out the same task twice, stop.&lt;/li&gt;
&lt;li&gt;The paper's own suggestion: occasionally spawn a worker that sees only &lt;code&gt;task.md&lt;/code&gt;. Inherited notes anchor every worker to an early wrong idea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My take: if you run a 27B locally, GVS5H is good news. Structure plus a test runner got a mid-size model to frontier-level pass@1 on hard problems. I wouldn't read it as evidence that a 27B is as smart as Fable 5, and I'd stop quoting the 9% figure next to the 27B.&lt;/p&gt;

&lt;p&gt;So is a 27B looping through up to 10 manager→worker rounds against one call of Fable 5 a fair fight? Or is that exactly the point, since your local GPU doesn't bill per token? Tell me in the comments, especially if you've run a ledger-style loop on your own hardware and watched it make things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GVS5H repo and paper source: &lt;a href="https://github.com/slee-persis/GVS5H" rel="noopener noreferrer"&gt;https://github.com/slee-persis/GVS5H&lt;/a&gt; (&lt;code&gt;paper/paper_latest.tex&lt;/code&gt;, &lt;code&gt;tab-4new-5pass.tex&lt;/code&gt;, &lt;code&gt;fig-cost-tables.tex&lt;/code&gt;, &lt;code&gt;tab-qwen-caps.tex&lt;/code&gt;, &lt;code&gt;tab-openrouter.tex&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Hacker News discussion: &lt;a href="https://news.ycombinator.com/item?id=50228167" rel="noopener noreferrer"&gt;https://news.ycombinator.com/item?id=50228167&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HTTP2Shell (CVE-2026-111497): pip-audit Missed Brotli. How to Check Your Stack</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:30:02 +0000</pubDate>
      <link>https://dev.to/alanwest/http2shell-cve-2026-111497-pip-audit-missed-brotli-how-to-check-your-stack-31e3</link>
      <guid>https://dev.to/alanwest/http2shell-cve-2026-111497-pip-audit-missed-brotli-how-to-check-your-stack-31e3</guid>
      <description>&lt;p&gt;If you ran pip-audit on Thursday, got a clean report and moved on, run it again. CVE-2026-111497 is an out-of-bounds write in Google's Brotli decoder. Aikido's headline now calls it HTTP2Shell; the rest of the internet mostly knew it as Node2Shell. It has a CVSS v4 score of 9.2 and affects Brotli 1.0.3 through 1.2.0. As of October 23, &lt;a href="https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497" rel="noopener noreferrer"&gt;Aikido reported&lt;/a&gt; that pip-audit, Dependabot and Safety were not flagging the vulnerable Python packages, because the advisory databases hadn't mapped the CVE to &lt;code&gt;Brotli&lt;/code&gt; or &lt;code&gt;brotlicffi&lt;/code&gt; on PyPI.&lt;/p&gt;

&lt;p&gt;That mapping has landed. Scanners are still a bad way to answer "am I exposed?" for this bug, because most of the vulnerable copies aren't Python packages at all. These are the checks I'd run instead, for Python, Node, Bun and Docker images, plus the fixed versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CVE-2026-111497 actually is
&lt;/h2&gt;

&lt;p&gt;The bug sits in &lt;code&gt;c/dec/decode.c&lt;/code&gt;. In the vulnerable versions, &lt;code&gt;DecodeBlockTypeAndLength()&lt;/code&gt; returns a plain boolean, so &lt;code&gt;BROTLI_FALSE&lt;/code&gt; can mean either "this stream is malformed" or "I need more input". The caller can't tell those apart. The &lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js advisory&lt;/a&gt; describes what happens next: an invalid block switch gets reported as a temporary need for more input, and the streaming decoder keeps appending bytes to an eight-byte internal buffer, writing past the end of it. Node lists crash, memory disclosure and remote code execution as possible outcomes.&lt;/p&gt;

&lt;p&gt;The upstream fix is google/brotli commit &lt;code&gt;0ed4268&lt;/code&gt;. It adds a new error code, &lt;code&gt;BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH&lt;/code&gt; (-17). According to the Aikido write-up, a researcher diffed 1.2.0 against 1.2.1 and found only the version bump plus that commit's decoder changes, then built both with AddressSanitizer. 1.2.0 consumed a roughly 23 MB crafted stream and kept asking for more, even when fed one byte at a time. 1.2.1 rejected it with -17 every time.&lt;/p&gt;

&lt;p&gt;Node credits OpenAI Codex Security (Khai Tran and Sylvie Mayer) with the report. Aikido says OpenAI found it in February and that the root cause was fixed in the Brotli source back then without anyone treating it as a security issue, so no release carried the fix until now. Commenters in the &lt;a href="https://news.ycombinator.com/item?id=50219177" rel="noopener noreferrer"&gt;HN thread&lt;/a&gt; doubted the February date, so I'd treat it as unconfirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't need HTTP to trigger it, but that's how you'll get hit
&lt;/h2&gt;

&lt;p&gt;The HN thread spent a lot of energy mocking "HTTP2Shell". It's a decompressor bug, and as pimterry pointed out there, you can trigger it with no HTTP involved by decoding Brotli yourself. Someone linked a rival name, Broccoli2Shell, which already has its own site. I like that one better.&lt;/p&gt;

&lt;p&gt;Still, most real exposure comes through HTTP content negotiation. Anything that decodes &lt;code&gt;Content-Encoding: br&lt;/code&gt; from a peer it doesn't trust can be hit, and that works in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inbound: a malicious client sends a Brotli-compressed request body to a server that decompresses it. Node apps with body parsing enabled are in this group, and so is ASP.NET with request decompression turned on.&lt;/li&gt;
&lt;li&gt;Outbound: a malicious or compromised server sends a Brotli response to a client that decodes it automatically. In Python that means requests, httpx and aiohttp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On HN, bawolff argues the outbound case is much less severe because the attacker first has to get your code to call their server. That's fair for a payment service that only ever calls Stripe. It's not fair for scrapers, webhook senders, link previewers, RSS fetchers, or anything that does &lt;code&gt;requests.get(user_supplied_url)&lt;/code&gt;. If your service fetches URLs that users give it, the attacker controls the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python: requests decodes Brotli if it's merely installed
&lt;/h2&gt;

&lt;p&gt;urllib3, and therefore requests, decodes Brotli whenever &lt;code&gt;Brotli&lt;/code&gt; or &lt;code&gt;brotlicffi&lt;/code&gt; happens to be importable. You don't opt in. In a venv with Brotli 1.1.0, requests 2.34.2 and urllib3 2.8.0, this is what requests sends by default:&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="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding&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;gzip, deflate, br&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uninstall Brotli and the &lt;code&gt;br&lt;/code&gt; disappears. httpx does the same with the &lt;code&gt;brotli&lt;/code&gt; extra, aiohttp does it when Brotli is installed, and Aikido lists Scrapy and anything built on these clients. The Python Brotli package gets 82.8 million downloads a month, and I doubt many of those come from people who typed &lt;code&gt;pip install brotli&lt;/code&gt; themselves.&lt;/p&gt;

&lt;p&gt;So check whether anything in the environment dragged Brotli in, and which version it brought. Your requests version won't tell you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check for vulnerable Brotli in 5 minutes
&lt;/h2&gt;

&lt;p&gt;Python, in every venv, CI runner and image you care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show Brotli brotlicffi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're vulnerable if &lt;code&gt;Brotli&lt;/code&gt; is below 1.2.1 or &lt;code&gt;brotlicffi&lt;/code&gt; is below 1.2.1.0 (&lt;a href="https://github.com/advisories/GHSA-9v8h-qqgm-4m35" rel="noopener noreferrer"&gt;GHSA-9v8h-qqgm-4m35&lt;/a&gt;). A &lt;code&gt;WARNING: Package(s) not found&lt;/code&gt; line for one of them is fine.&lt;/p&gt;

&lt;p&gt;To find out who pulled it in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipdeptree &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; Brotli
pipdeptree &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; brotlicffi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lockfiles are faster than spinning up every environment:&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;-i&lt;/span&gt; brotli poetry.lock uv.lock requirements&lt;span class="k"&gt;*&lt;/span&gt;.txt 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container images are where it tends to turn up. Override the entrypoint so the image's own startup command doesn't get in the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; pip &amp;lt;image&amp;gt; show Brotli brotlicffi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node and Bun:&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;-v&lt;/span&gt;   &lt;span class="c"&gt;# need 22.23.4, 24.22.1 or 26.12.1+&lt;/span&gt;
bun &lt;span class="nt"&gt;-v&lt;/span&gt;    &lt;span class="c"&gt;# need 1.4.4+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 20 reached end of life on April 30, 2026, and EOL lines don't get security releases. Node bundles its own Brotli, so your OS package doesn't matter there. The same goes for single-executable apps and compiled Bun binaries: they carry their own copy and have to be rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed versions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;th&gt;Gotcha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;22.23.4, 24.22.1, 26.12.1&lt;/td&gt;
&lt;td&gt;Rebuild images and SEA binaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libbrotli&lt;/td&gt;
&lt;td&gt;1.2.1 (or 1.0.10 / 1.1.1 on older lines)&lt;/td&gt;
&lt;td&gt;Distros may backport without a version bump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python &lt;code&gt;Brotli&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.2.1&lt;/td&gt;
&lt;td&gt;Regenerate lockfiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brotlicffi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.2.1.0&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;1.4.4&lt;/td&gt;
&lt;td&gt;Recompile &lt;code&gt;bun build --compile&lt;/code&gt; outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Envoy&lt;/td&gt;
&lt;td&gt;1.40.0, 1.39.3, 1.38.6, 1.37.8&lt;/td&gt;
&lt;td&gt;Sidecars update with the mesh, not your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Istio&lt;/td&gt;
&lt;td&gt;1.29.9, 1.30.6, 1.31.2&lt;/td&gt;
&lt;td&gt;Check the Envoy version it ships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;curl / libcurl&lt;/td&gt;
&lt;td&gt;Whatever your vendor ships against Brotli 1.2.1&lt;/td&gt;
&lt;td&gt;Static builds need a rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Python:&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; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"Brotli&amp;gt;=1.2.1"&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"brotlicffi&amp;gt;=1.2.1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then regenerate the Poetry, uv or pip-tools lockfile and rebuild every image that contains it. Updating the package on your laptop doesn't patch the container that shipped last Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does pip-audit catch CVE-2026-111497 now?
&lt;/h2&gt;

&lt;p&gt;For the PyPI packages, yes. The &lt;a href="https://osv.dev/vulnerability/GHSA-9v8h-qqgm-4m35" rel="noopener noreferrer"&gt;OSV entry&lt;/a&gt; now lists &lt;code&gt;brotli&lt;/code&gt; and &lt;code&gt;brotlicffi&lt;/code&gt; with aliases PYSEC-2026-4322 and PYSEC-2026-4323. The advisory was published on October 22 and last modified on October 24, 2026. If your weekly scan ran before the mapping showed up, you have a green checkmark sitting on top of a 9.2.&lt;/p&gt;

&lt;p&gt;My bigger complaint is about everything pip-audit was never going to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Brotli statically linked into your Node binary&lt;/li&gt;
&lt;li&gt;the copy inside a compiled Bun executable&lt;/li&gt;
&lt;li&gt;Envoy sidecars in your Istio mesh&lt;/li&gt;
&lt;li&gt;libbrotli under a statically built curl&lt;/li&gt;
&lt;li&gt;distro packages where the fix was backported and the version string didn't change (Ubuntu's libbrotli1 fix was still under review at medium severity when the HN thread was going)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People in the HN thread also listed Git for Windows, Wireshark, Visual Studio and .NET as shipping Brotli. A Python dependency scanner reports on Python dependencies, and this bug lives in a C library that roughly half your stack vendors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround if you can't patch today: drop &lt;code&gt;br&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Inbound: reject any request whose &lt;code&gt;Content-Encoding&lt;/code&gt; contains &lt;code&gt;br&lt;/code&gt; at the proxy, load balancer or middleware, before anything decodes it, and match case-insensitively. Nginx and Apache don't decode Brotli request bodies out of the box (Apache's &lt;code&gt;mod_brotli&lt;/code&gt; only compresses), which makes them a reasonable place to block it.&lt;/li&gt;
&lt;li&gt;Outbound, Python: uninstall &lt;code&gt;Brotli&lt;/code&gt; and &lt;code&gt;brotlicffi&lt;/code&gt;, remove extras like &lt;code&gt;httpx[brotli]&lt;/code&gt;, regenerate locks and restart. Clients fall back to gzip and deflate. This does nothing for Brotli bundled into other runtimes.&lt;/li&gt;
&lt;li&gt;Outbound, generally: removing &lt;code&gt;br&lt;/code&gt; from &lt;code&gt;Accept-Encoding&lt;/code&gt; is not enough. A hostile server can send Brotli whether you asked for it or not, so you have to reject &lt;code&gt;br&lt;/code&gt; responses before the body gets decoded automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone on HN asked whether a TLS-terminating load balancer in front of a Node app protects you. The answer in the thread was no: terminating TLS has nothing to do with decoding bodies, and most load balancers pass the body through untouched. Unless yours is explicitly configured to drop &lt;code&gt;br&lt;/code&gt;, assume it does nothing.&lt;/p&gt;

&lt;p&gt;The Hacker News (the news site, not the forum) reported no exploitation in the wild as of October 22. Aikido also notes that researchers have already exploited the same decoder code against an unmodified Node.js 24.22.0 build, so I wouldn't count on that staying true for long.&lt;/p&gt;

&lt;p&gt;Did your scanner flag Brotli, or did you find it in a container image by hand? I'm curious how many copies people turned up compared to the number of services they actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497" rel="noopener noreferrer"&gt;Aikido: Node2Shell evolves to HTTP2Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js October 2026 security releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/advisories/GHSA-9v8h-qqgm-4m35" rel="noopener noreferrer"&gt;GHSA-9v8h-qqgm-4m35&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://osv.dev/vulnerability/GHSA-9v8h-qqgm-4m35" rel="noopener noreferrer"&gt;OSV entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=50219177" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>GPT-6 on Google Cloud Vertex AI: Your $300 Free Credit Can't Pay for It</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:10:00 +0000</pubDate>
      <link>https://dev.to/alanwest/gpt-6-on-google-cloud-vertex-ai-your-300-free-credit-cant-pay-for-it-1363</link>
      <guid>https://dev.to/alanwest/gpt-6-on-google-cloud-vertex-ai-your-300-free-credit-cant-pay-for-it-1363</guid>
      <description>&lt;p&gt;Half my feed this week was some version of "GPT-6 is on Google Cloud Vertex AI now, go burn your $300 free credit on it." I went through the docs to set that up. You can't, for two reasons, and Google's own pages spell out both.&lt;/p&gt;

&lt;p&gt;Below is what's gated, the one OpenAI model you can call on Vertex AI right now with the OpenAI Python SDK, and how GPT-6 pricing compares across the OpenAI API, Azure and Google Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google launched: GPT-6 in private preview
&lt;/h2&gt;

&lt;p&gt;Google's &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/google-cloud-brings-openai-models-to-customers" rel="noopener noreferrer"&gt;launch post&lt;/a&gt; (dated October 22, 2026) adds three closed OpenAI models to Model Garden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-6 Astra, the flagship&lt;/li&gt;
&lt;li&gt;GPT-6.1 Sol, "near-Astra intelligence at lower cost"&lt;/li&gt;
&lt;li&gt;GPT-6 Luna, the cheap high-volume one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The status line is the part people skipped: "rolling out now in private preview to select enterprise customers." Access starts with an &lt;a href="https://cloud.google.com/resources/interest/openai-models" rel="noopener noreferrer"&gt;interest form&lt;/a&gt;. OpenAI's &lt;a href="https://openai.com/index/openai-models-google-cloud/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt; says general availability is planned "in the coming weeks," &lt;a href="https://thenextweb.com/news/openai-models-codex-google-cloud" rel="noopener noreferrer"&gt;per The Next Web&lt;/a&gt;. Google gives no date at all.&lt;/p&gt;

&lt;p&gt;As of this week Google hasn't published a model ID, a region list, a quota table or a per-token price for any of the three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the $300 free credit won't cover GPT-6
&lt;/h2&gt;

&lt;p&gt;Even if you clear the preview gate, the trial account can't pay for these models. Google's &lt;a href="https://docs.cloud.google.com/free/docs/free-cloud-features" rel="noopener noreferrer"&gt;free trial page&lt;/a&gt; says:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On a non-billable Free Trial account you can't use Google Cloud Marketplace at all. Google sells the OpenAI models through Marketplace.&lt;/li&gt;
&lt;li&gt;"You can't access or use the $300 credit for a generative AI partner model that is offered as a managed API."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same page says trial accounts can't request quota increases either. So "try GPT-6 on free credits" is out, before you even get to the preview waitlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can call today: gpt-oss-120b on Vertex AI with the OpenAI SDK
&lt;/h2&gt;

&lt;p&gt;OpenAI's open-weight models have been on Vertex AI since August 13, 2025 as serverless APIs. &lt;code&gt;gpt-oss-120b-maas&lt;/code&gt; is GA, does function calling and structured output, and has a 131,072-token context (&lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/openai/gpt-oss-120b" rel="noopener noreferrer"&gt;model card&lt;/a&gt;). The model card lists &lt;code&gt;global&lt;/code&gt; for availability with processing in the &lt;code&gt;us&lt;/code&gt; multi-region, and quotas for &lt;code&gt;us-central1&lt;/code&gt;, which is what the snippet below uses. Google files it under "open models," not "partner models." Whether that puts it inside the free-credit rules is something I couldn't get a straight answer on from the docs, so check your billing report after the first few calls.&lt;/p&gt;

&lt;p&gt;Setup takes three steps. First, grant two IAM roles (&lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/grant-access-open-models" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). One lets someone enable the model in Model Garden, and the other lets them send prompts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud projects add-iam-policy-binding PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user:you@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;roles/consumerprocurement.entitlementManager

gcloud projects add-iam-policy-binding PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user:you@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;roles/aiplatform.user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your org restricts service usage, an admin has to allow &lt;code&gt;cloudcommerceconsumerprocurement.googleapis.com&lt;/code&gt;, or enabling the model fails.&lt;/p&gt;

&lt;p&gt;Next, enable gpt-oss in Model Garden from the console and set up Application Default Credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then call it with the OpenAI SDK, passing the access token as the API key. The code follows the pattern from Google's &lt;a href="https://discuss.google.dev/t/now-ga-openais-gpt-oss-qwen3-models-on-vertex-ai-as-open-model-apis/253945" rel="noopener noreferrer"&gt;GA post&lt;/a&gt; and the &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/call-open-model-apis" rel="noopener noreferrer"&gt;Chat Completions docs&lt;/a&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;google.auth&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;google.auth.transport.requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;

&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-project-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;LOCATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LOCATION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-aiplatform.googleapis.com/v1/projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/locations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LOCATION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/endpoints/openapi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Google access token, not an OpenAI key
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-oss-120b-maas&lt;/span&gt;&lt;span class="sh"&gt;"&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;Explain a serverless API in one analogy.&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas. Access tokens expire after about an hour, so a long-running service needs to refresh &lt;code&gt;creds&lt;/code&gt; and rebuild the client. And the docs say the &lt;code&gt;global&lt;/code&gt; endpoint (&lt;code&gt;https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/endpoints/openapi&lt;/code&gt;) costs the same as a regional one and can cut errors, but use regional if data residency matters. The org policy &lt;code&gt;constraints/gcp.restrictEndpointUsage&lt;/code&gt; can also block global.&lt;/p&gt;

&lt;p&gt;Third-party trackers such as &lt;a href="https://www.llmreference.com/model/gpt-oss-120b/gcp-vertex-ai" rel="noopener noreferrer"&gt;LLM Reference&lt;/a&gt; list Vertex's gpt-oss-120b price at $0.09 input and $0.36 output per million tokens. Google's pricing page wouldn't render for me, so treat that as unconfirmed.&lt;/p&gt;

&lt;p&gt;I'd also guess GPT-6 lands on this same &lt;code&gt;openapi&lt;/code&gt; endpoint once it's GA, so this code would only need a new model string. That's only a guess, though, because Google hasn't published the GPT-6 integration docs yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  GPT-6 pricing: OpenAI API vs Azure vs Google Cloud
&lt;/h2&gt;

&lt;p&gt;Short-context list prices per million tokens (input / output), from &lt;a href="https://developers.openai.com/api/docs/pricing" rel="noopener noreferrer"&gt;OpenAI's pricing page&lt;/a&gt; and &lt;a href="https://azure.microsoft.com/en-us/blog/gpt-6-astra-sol-and-luna-for-production-agents-in-microsoft-foundry/" rel="noopener noreferrer"&gt;Microsoft's Foundry launch post&lt;/a&gt;:&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;OpenAI API&lt;/th&gt;
&lt;th&gt;Azure Foundry&lt;/th&gt;
&lt;th&gt;Google Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;td&gt;GA (Astra since Sept. 3, Sol and Luna since Sept. 22)&lt;/td&gt;
&lt;td&gt;Private preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;$10 / $50&lt;/td&gt;
&lt;td&gt;$10 / $50 Global&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;
&lt;td&gt;$2 / $10&lt;/td&gt;
&lt;td&gt;Not listed (GPT-6 Sol: $2 / $10 Global)&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Luna&lt;/td&gt;
&lt;td&gt;$0.10 / $0.50&lt;/td&gt;
&lt;td&gt;$0.10 / $0.50 Global&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch discount&lt;/td&gt;
&lt;td&gt;50% (Batch and Flex)&lt;/td&gt;
&lt;td&gt;Not in launch post&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data residency cost&lt;/td&gt;
&lt;td&gt;+10% on regional endpoints&lt;/td&gt;
&lt;td&gt;US Data Zone +10%, EU Data Zone +20%&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions&lt;/td&gt;
&lt;td&gt;Regional endpoints&lt;/td&gt;
&lt;td&gt;28 Global regions plus US and EU Data Zones&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bills against&lt;/td&gt;
&lt;td&gt;OpenAI account&lt;/td&gt;
&lt;td&gt;Azure commit&lt;/td&gt;
&lt;td&gt;GCP commit, Marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that Azure sells GPT-6 Sol, not GPT-6.1 Sol. OpenAI sells both at $2 / $10, but cached input differs: $0.10 for GPT-6.1 Sol versus $0.20 for GPT-6 Sol, which is also what Azure charges. For context, when AWS took GPT-5.6 to GA on Bedrock it &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/07/openai-gpt-sol-terra/" rel="noopener noreferrer"&gt;said&lt;/a&gt; "Pricing matches OpenAI first-party rates." I'd expect Google to match OpenAI's list price too, but nobody has put that in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  GCP commits and Flexible Savings Plans: drawdown, no discount
&lt;/h2&gt;

&lt;p&gt;The reason to care is one sentence in Google's post: usage "draws down existing cloud commitments, and counts toward Flexible Savings Plans (FSP)." If you're behind on a GCP commit, that sentence matters more than anything else in the announcement.&lt;/p&gt;

&lt;p&gt;What it doesn't get you is a discount. Google's &lt;a href="https://docs.cloud.google.com/docs/cuds-flexible-savings-plans" rel="noopener noreferrer"&gt;FSP docs&lt;/a&gt; say "Third-party products don't get any discounts from the FSP," and "Underutilized FSP commitments don't carry forward." The one exception is a Marketplace Private Offer: if you negotiate a discount off the partner's list price, that discount applies, but you'll have to get it out of your sales rep. One &lt;a href="https://news.ycombinator.com/item?id=50180079" rel="noopener noreferrer"&gt;Hacker News commenter&lt;/a&gt; says partner spend can only cover up to 25% of a commitment. I couldn't find that in Google's public docs, so ask your account rep before you plan around it.&lt;/p&gt;

&lt;p&gt;Security has gaps too. ZDR and Confidential Compute are both "coming soon" in the launch post, and Vertex's &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/security-controls" rel="noopener noreferrer"&gt;security controls page&lt;/a&gt; says "Security controls are not supported for preview models." The launch post also claims VPC Service Controls protection. Until GA, I'd go with the narrower docs page.&lt;/p&gt;

&lt;p&gt;Capacity is the other open question. In the same HN thread, one commenter says Gemini's dynamic usage limits are "hitting us hard," and another reports a month of compute allocation failures in us-central1 on ordinary Dataflow-type jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;If you're an enterprise with a fat GCP commit and you're under-consumed, fill out the interest form today. Moving OpenAI spend onto that invoice is free money in the "use it or lose it" sense, even at list price.&lt;/p&gt;

&lt;p&gt;Anyone else should wait. Azure has had all three models GA since September with published prices and two residency tiers. OpenAI direct is the same price with fewer IAM roles to deal with. Google Cloud is the third place to buy the same tokens, and it's the only one where you can't see the price yet.&lt;/p&gt;

&lt;p&gt;If you just want to try OpenAI weights on GCP this afternoon, gpt-oss-120b and the snippet above will get you there.&lt;/p&gt;

&lt;p&gt;Would you move your OpenAI billing to Google Cloud or Azure, or keep paying OpenAI directly? And if your finance team has already forced the move to burn down a commit, I'd like to hear how the quotas held up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud launch post: &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/google-cloud-brings-openai-models-to-customers" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/products/ai-machine-learning/google-cloud-brings-openai-models-to-customers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI announcement: &lt;a href="https://openai.com/index/openai-models-google-cloud/" rel="noopener noreferrer"&gt;https://openai.com/index/openai-models-google-cloud/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud free trial terms: &lt;a href="https://docs.cloud.google.com/free/docs/free-cloud-features" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/free/docs/free-cloud-features&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vertex AI gpt-oss-120b model card: &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/openai/gpt-oss-120b" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/openai/gpt-oss-120b&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Call MaaS APIs for open models: &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/call-open-model-apis" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/call-open-model-apis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Grant access to open models: &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/grant-access-open-models" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/vertex-ai/generative-ai/docs/maas/grant-access-open-models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI API pricing: &lt;a href="https://developers.openai.com/api/docs/pricing" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Foundry GPT-6 pricing: &lt;a href="https://azure.microsoft.com/en-us/blog/gpt-6-astra-sol-and-luna-for-production-agents-in-microsoft-foundry/" rel="noopener noreferrer"&gt;https://azure.microsoft.com/en-us/blog/gpt-6-astra-sol-and-luna-for-production-agents-in-microsoft-foundry/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Flexible Savings Plans: &lt;a href="https://docs.cloud.google.com/docs/cuds-flexible-savings-plans" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/docs/cuds-flexible-savings-plans&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hacker News discussion: &lt;a href="https://news.ycombinator.com/item?id=50180079" rel="noopener noreferrer"&gt;https://news.ycombinator.com/item?id=50180079&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>openai</category>
      <category>googlecloud</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
