<?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: Google Developer Experts</title>
    <description>The latest articles on DEV Community by Google Developer Experts (gde).</description>
    <link>https://dev.to/gde</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%2Forganization%2Fprofile_image%2F11939%2Fe3080d5b-ecde-42a8-b089-bafecc31fa97.png</url>
      <title>DEV Community: Google Developer Experts</title>
      <link>https://dev.to/gde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gde"/>
    <language>en</language>
    <item>
      <title>Dart 3.13 Primary Constructors + BlocSignal: Boilerplate-Free Reactive Architecture</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Fri, 14 Aug 2026 02:57:56 +0000</pubDate>
      <link>https://dev.to/gde/dart-313-primary-constructors-blocsignal-boilerplate-free-reactive-architecture-5fll</link>
      <guid>https://dev.to/gde/dart-313-primary-constructors-blocsignal-boilerplate-free-reactive-architecture-5fll</guid>
      <description>&lt;p&gt;For years, one of the most common critiques of the BLoC pattern has been &lt;strong&gt;boilerplate&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Between declaring event classes, state hierarchies, constructor parameters, private fields, super-initializers, and event handler registries, you could easily write 50 lines of code before handling a single real-world user action.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Dart 3.13&lt;/strong&gt;, that all changes. &lt;/p&gt;

&lt;p&gt;Dart 3.13 brings &lt;strong&gt;Primary Constructors&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;this&lt;/code&gt; constructor body blocks&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;new&lt;/code&gt;/&lt;code&gt;factory&lt;/code&gt; constructor shorthands&lt;/strong&gt;. When combined with &lt;strong&gt;&lt;a href="https://blocsignal.dev" rel="noopener noreferrer"&gt;BlocSignal&lt;/a&gt;&lt;/strong&gt;—the synchronous, signals-powered evolution of BLoC—the result is an ultra-concise, fully type-safe, and boilerplate-free state management workflow.&lt;/p&gt;

&lt;p&gt;Let's explore how Dart 3.13 and BlocSignal fit together like hand in glove.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zero-Boilerplate Events &amp;amp; States
&lt;/h2&gt;

&lt;p&gt;In classic BLoC, defining a family of immutable events or states meant writing repeated constructor signatures and field definitions for every subtype.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔴 Before Dart 3.13:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserFetchRequested&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;UserFetchRequested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserUpdated&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;UserUpdated&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserLoggedOut&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🟢 With Dart 3.13 Primary Constructors:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;UserFetchRequested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;UserUpdated&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;UserLoggedOut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;UserEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A whole sealed hierarchy of events or states can now be declared in just a few clean, expressive lines without losing type safety or exhaustiveness checking in &lt;code&gt;switch&lt;/code&gt; expressions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Streamlined Dependency Injection in &lt;code&gt;CubitSignal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;CubitSignal&lt;/code&gt;, you typically inject repositories, API clients, or analytic trackers. In previous Dart versions, you had to declare each field, accept constructor arguments, and forward initial state to &lt;code&gt;super&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With primary constructors, field declarations and super invocations live right in the class header.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔴 Before Dart 3.13:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCubit&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CubitSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AnalyticsService&lt;/span&gt; &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;UserCubit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AnalyticsService&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UserState&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserInitial&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_loaded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🟢 With Dart 3.13:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;UserCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AnalyticsService&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserState&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserInitial&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CubitSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_loaded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No field re-declarations. No duplicate parameter names. The dependencies are immediately available across all methods.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Event Handler Registration via the &lt;code&gt;this&lt;/code&gt; Block in &lt;code&gt;BlocSignal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most powerful features in Dart 3.13 is the &lt;strong&gt;&lt;code&gt;this&lt;/code&gt; constructor body syntax&lt;/strong&gt;. When using primary constructors, constructor body logic (such as registering event handlers with &lt;code&gt;on&amp;lt;E&amp;gt;()&lt;/code&gt; or asserting preconditions) is placed inside a &lt;code&gt;this { ... }&lt;/code&gt; block in the class body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;SearchBloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SearchRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SearchState&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SearchInitial&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;BlocSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SearchEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Dart 3.13 primary constructor body&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SearchQueryChanged&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SearchEmpty&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SearchLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SearchSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;restartable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Zero-stream event concurrency!&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header cleanly declares the class contract, and the &lt;code&gt;this&lt;/code&gt; block sets up the event pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Immediate Reactive Wiring with &lt;code&gt;createEffect&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BlocSignal&lt;/code&gt; includes &lt;code&gt;createEffect&lt;/code&gt;, which automatically tracks signal dependencies and manages teardown on container disposal. With primary constructor parameters in scope, derived cubits can synchronously wire up upstream state containers in the &lt;code&gt;this&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;CartSummaryCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;CartBloc&lt;/span&gt; &lt;span class="n"&gt;cartBloc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CubitSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CartSummary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CartSummary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Automatically reacts to cartBloc.state signals synchronously:&lt;/span&gt;
    &lt;span class="n"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cartBloc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CartSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;total:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Named Constructor Shorthands (&lt;code&gt;new&lt;/code&gt;) for Testing &amp;amp; Seeding
&lt;/h2&gt;

&lt;p&gt;Dart 3.13 also introduces constructor shorthands, allowing you to define secondary named constructors using &lt;code&gt;new name()&lt;/code&gt; without repeating the class name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;CounterCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CubitSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Named constructor shorthands:&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes testing variations, mock seeds, and default configurations concise and readable.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Enabling Dart 3.13 in Your Project
&lt;/h2&gt;

&lt;p&gt;To take advantage of these features:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Set the SDK Constraint in &lt;code&gt;pubspec.yaml&lt;/code&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^3.13.0&lt;/span&gt;

&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bloc_signals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;bloc_signals_flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Enable Dart 3.13 Linter Rules in &lt;code&gt;analysis_options.yaml&lt;/code&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package:very_good_analysis/analysis_options.yaml&lt;/span&gt;

&lt;span class="na"&gt;linter&lt;/span&gt;&lt;span class="pi"&gt;:&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="s"&gt;use_primary_constructors&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;use_declaring_parameters&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unnecessary_type_name_in_constructor&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unnecessary_primary_constructor_body&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚀 The Architectural Payoff
&lt;/h2&gt;

&lt;p&gt;By combining &lt;strong&gt;Dart 3.13&lt;/strong&gt; language features with &lt;strong&gt;BlocSignal&lt;/strong&gt;, you get:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;0ms Synchronous Updates&lt;/strong&gt;: State emissions propagate in the current frame without microtask delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Ceremony&lt;/strong&gt;: Class headers declare fields and super initializers simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal Graph Efficiency&lt;/strong&gt;: Automatic &lt;code&gt;==&lt;/code&gt; de-duplication and fine-grained UI rebuilding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard BLoC Rigor&lt;/strong&gt;: Clean event dispatching, state transitions, and OpenTelemetry observability.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💬 Over to You: What's Your Take?
&lt;/h2&gt;

&lt;p&gt;We'd love to hear your thoughts in the comments below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How do you feel about Dart 3.13's primary constructors?&lt;/strong&gt; Does declaring fields directly in the class header match how you design your domain and state layers?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you planning to adopt primary constructors across your state management classes&lt;/strong&gt;, or are there specific patterns where you still prefer classic constructors?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have a boilerplate-heavy state class or Bloc?&lt;/strong&gt; Drop a snippet in the comments, and let's see how much code Dart 3.13 and BlocSignal can shave off!&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Ready to build boilerplate-free reactive apps? &lt;/p&gt;

&lt;p&gt;Check out the full documentation, benchmarks, and interactive examples at &lt;strong&gt;&lt;a href="https://blocsignal.dev" rel="noopener noreferrer"&gt;blocsignal.dev&lt;/a&gt;&lt;/strong&gt; or star the open-source repository on &lt;strong&gt;&lt;a href="https://github.com/RandalSchwartz/BlocSignal" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>statemanagement</category>
      <category>programming</category>
    </item>
    <item>
      <title>Running Gemma 4 on EC2 G5g: Graviton2 AMD with NVIDIA GPU</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:42:09 +0000</pubDate>
      <link>https://dev.to/gde/running-gemma-4-on-ec2-g5g-graviton2-amd-with-nvidia-gpu-25ci</link>
      <guid>https://dev.to/gde/running-gemma-4-on-ec2-g5g-graviton2-amd-with-nvidia-gpu-25ci</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frnpzj33gf9hm5ae8x0qt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frnpzj33gf9hm5ae8x0qt.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A field report on serving Google's Gemma 4 E2B on AWS EC2 **G5g&lt;/em&gt;* — a Graviton2 (aarch64)&lt;br&gt;
host with an NVIDIA &lt;strong&gt;T4G&lt;/strong&gt; (Turing, SM 7.5) GPU. Three obstacles: an &lt;strong&gt;arch list&lt;/strong&gt; nobody&lt;br&gt;
publishes for this combination, a &lt;strong&gt;version floor&lt;/strong&gt; that only the newest vLLM clears, and&lt;br&gt;
&lt;strong&gt;64 KiB of shared memory&lt;/strong&gt; that stops the model dead. Plus the seven things I documented&lt;br&gt;
wrong before I had a box.*&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; (reference bf16 release)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;AWS EC2 &lt;code&gt;g5g.4xlarge&lt;/code&gt; — Graviton2 + 1x NVIDIA T4G, compute capability &lt;strong&gt;7.5&lt;/strong&gt;, 15,360 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base image&lt;/td&gt;
&lt;td&gt;Deep Learning ARM64 AMI OSS Nvidia Driver GPU PyTorch 2.12 (Ubuntu 24.04)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software&lt;/td&gt;
&lt;td&gt;torch 2.12.0+cu132 · CUDA 13.2 · vLLM v0.27.2rc0 built from source for &lt;code&gt;sm_75&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;43.1 tok/s&lt;/strong&gt; single-stream greedy, 329,579-token KV cache — after one patch to vLLM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;p&gt;G5g is the only instance AWS has ever shipped that puts an NVIDIA GPU behind a Graviton&lt;br&gt;
host. It launched in 2020, it never got a successor, and Graviton is now on its fifth&lt;br&gt;
generation without one.&lt;/p&gt;

&lt;p&gt;That matters more than it sounds. The Arm-plus-CUDA world moved on to NVIDIA's own Arm CPU&lt;br&gt;
— Grace, paired with SM 9.0 and 10.0 parts. Turing stayed well supported, on x86. G5g is&lt;br&gt;
the only hardware that is aarch64 &lt;em&gt;and&lt;/em&gt; compute capability 7.5, and almost nobody publishes&lt;br&gt;
a build for that combination.&lt;/p&gt;

&lt;p&gt;I put a rig on one anyway. &lt;strong&gt;The packaging problem was the quick part.&lt;/strong&gt; Everything after it&lt;br&gt;
— a compiler that was not there, a version floor I did not expect, and 32 KiB of shared&lt;br&gt;
memory — took far longer, because none of it fails where you are looking.&lt;/p&gt;
&lt;h2&gt;
  
  
  No published build covers aarch64 and SM 7.5 together
&lt;/h2&gt;

&lt;p&gt;Start with the obvious candidate. &lt;code&gt;vllm/vllm-openai:v0.27.1&lt;/code&gt; publishes both platforms under&lt;br&gt;
one tag, and you can read the arch lists straight out of the image config without pulling a&lt;br&gt;
layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx imagetools inspect vllm/vllm-openai:v0.27.1 &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .Image}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linux/amd64   7.5 8.0 8.6 8.9 9.0 10.0 12.0
linux/arm64       8.0 8.7 8.9 9.0 10.0 11.0 12.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one architecture this hardware needs is the only entry the two images disagree on. The&lt;br&gt;
arm64 list is Ampere and up, because that is what ships as an Arm-plus-NVIDIA system: A100,&lt;br&gt;
Jetson Orin, GH200, Blackwell. Turing is not on that list and never will be.&lt;/p&gt;

&lt;p&gt;Normally a missing target degrades to JIT from embedded PTX. Not here. The Dockerfile says&lt;br&gt;
so, with a comment:&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;# Do not add +PTX here: vLLM filters torch's top-level PTX flag when it&lt;/span&gt;
&lt;span class="c"&gt;# converts global gencode flags into per-kernel arch lists.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it does not run slowly. It fails outright, with &lt;code&gt;no kernel image is available for&lt;br&gt;
execution on the device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rest of the ecosystem splits the same way. Check before you plan anything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;7.5 on arm64&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;vllm/vllm-openai&lt;/code&gt; arm64&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Current. Never had it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nvcr.io/nvidia/pytorch&lt;/code&gt; arm64&lt;/td&gt;
&lt;td&gt;through 24.10&lt;/td&gt;
&lt;td&gt;Dropped by 24.12.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drikster80/vllm-aarch64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Abandoned Sept 2024. vLLM 0.6.1, far too old for Gemma 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyPI torch aarch64&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Built for 9.0 / 10.0 / 12.0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS ARM64 GPU DLAMI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maintained. PyTorch 2.2 through 2.12.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  AWS ships the one PyTorch that still has Turing
&lt;/h2&gt;

&lt;p&gt;This is the finding that saves the whole exercise, and I nearly wrote it off. I had assumed&lt;br&gt;
PyTorch's aarch64 CUDA wheels lacked &lt;code&gt;sm_75&lt;/code&gt; and that a from-source PyTorch build was&lt;br&gt;
coming. That is true of the PyPI wheels. It is not true of AWS.&lt;/p&gt;

&lt;p&gt;Read on two different DLAMIs, on the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;torch&lt;/span&gt; &lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cu128&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_75&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;sm_90&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;sm_100&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;sm_120&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt; &lt;span class="mf"&gt;2.12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cu132&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_75&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;sm_80&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;sm_90&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;sm_100&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;sm_110&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;sm_120&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;AWS sells G5g, so AWS keeps Turing in the build — right through PyTorch 2.12 on CUDA 13.2,&lt;br&gt;
an image cut three months ago. &lt;strong&gt;PyTorch never needs building.&lt;/strong&gt; Only vLLM's own kernels do,&lt;br&gt;
and CMake takes the arch list without argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;-- CUDA target architectures: 7.5
CMake Warning: Pytorch version 2.11.0 expected for CUDA build, saw 2.12.0 instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That warning is worth reading twice, and I come back to it below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PyTorch DLAMI has no compiler
&lt;/h2&gt;

&lt;p&gt;Two things the DLAMI does not give you, neither of them documented anywhere I could find.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;nvcc&lt;/code&gt;. The image ships the driver and a torch built against CUDA, not the&lt;br&gt;
toolkit. You need the keyring and &lt;code&gt;cuda-toolkit-13-2&lt;/code&gt; from NVIDIA's &lt;strong&gt;sbsa&lt;/strong&gt; repo — not the&lt;br&gt;
x86 one, which is an easy reflex to get wrong on an Arm box.&lt;/p&gt;

&lt;p&gt;And vLLM now wants Rust. Its &lt;code&gt;vllm-rs&lt;/code&gt; frontend needs &lt;code&gt;setuptools_rust&lt;/code&gt; plus a toolchain,&lt;br&gt;
and the failure is a bare &lt;code&gt;ModuleNotFoundError: No module named 'setuptools_rust'&lt;/code&gt; thrown&lt;br&gt;
from metadata generation, several minutes in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The newest vLLM was the only one that worked
&lt;/h2&gt;

&lt;p&gt;No vLLM tag pins torch 2.12. They go 2.11, then jump to 2.13. I reasoned that building older&lt;br&gt;
code against a newer runtime was the safer direction, took v0.26.0, and spent an hour being&lt;br&gt;
wrong about it.&lt;/p&gt;

&lt;p&gt;It builds fine. It then dies on model load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transformers.integrations.heterogeneity.configuration_utils.AmbiguousGlobalPerLayerAttributeError:
'head_dim' is a per-layer attribute and may vary across layers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4's &lt;code&gt;head_dim&lt;/code&gt; is not one number, and current &lt;code&gt;transformers&lt;/code&gt; refuses to hand out a&lt;br&gt;
global value for it. vLLM's config converter was still doing a flat&lt;br&gt;
&lt;code&gt;getattr(config, "head_dim", 0)&lt;/code&gt;. The &lt;code&gt;per_layer_config&lt;/code&gt; handling that copes with it landed&lt;br&gt;
in &lt;strong&gt;v0.27.2rc0&lt;/strong&gt; — not v0.27.1, which I also checked. The newest tag was the only one that&lt;br&gt;
worked.&lt;/p&gt;

&lt;p&gt;If you take one process lesson from this: reach for the latest release first, and make the&lt;br&gt;
constraint say out loud what stopped you when you fall back.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gemma 4's attention heads are not one size
&lt;/h2&gt;

&lt;p&gt;With the build working the server still would not start, and this failure has nothing to do&lt;br&gt;
with Arm or packaging. It is this model against this chip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gemma4 model has heterogeneous head dimensions
{'sliding_attention': 256, 'full_attention': 512}.
FA4 not available, forcing TRITON_ATTN backend.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that as a chain, because every link is load-bearing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gemma 4's sliding layers are 256 wide. Its global layers are &lt;strong&gt;512&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Only FA4 or Triton support heterogeneous head dims at all.&lt;/li&gt;
&lt;li&gt;FA4 is not available, so vLLM forces &lt;code&gt;TRITON_ATTN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;That choice is not yours to make. &lt;code&gt;VLLM_ATTENTION_BACKEND&lt;/code&gt; is not a recognised variable
in v0.27 — it logs &lt;code&gt;Unknown vLLM environment variable detected&lt;/code&gt; and carries on. I set it
twice before I read the warning.&lt;/li&gt;
&lt;li&gt;Triton's unified attention kernel at &lt;code&gt;head_size=512&lt;/code&gt; wants about 96 KiB of shared memory
per block.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  64 KiB is the whole problem
&lt;/h2&gt;

&lt;p&gt;Turing's shared memory is two numbers, and both are real. The &lt;strong&gt;default&lt;/strong&gt; static limit per block&lt;br&gt;
is 48 KiB — that is what &lt;code&gt;torch.cuda.get_device_properties().shared_memory_per_block&lt;/code&gt; reports,&lt;br&gt;
49,152 bytes. A kernel that needs more has to opt in through the dynamic shared-memory&lt;br&gt;
attribute, and even then it tops out at &lt;strong&gt;64 KiB&lt;/strong&gt;. Ampere and later have 164 KiB and up.&lt;/p&gt;

&lt;p&gt;Triton opts in, so it is measuring against the 64 KiB ceiling. It still does not fit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;triton.runtime.errors.OutOfResources: out of resource: shared memory,
Required: 98304, Hardware limit: 65536
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refused outright. Not slow, not degraded — the kernel will not launch, and it takes the&lt;br&gt;
engine down during CUDA graph capture, which is late enough that you have already watched&lt;br&gt;
the weights load and the KV cache get sized.&lt;/p&gt;

&lt;p&gt;The fix is small. Shrink the KV tile until the query block and the K/V tiles fit inside the&lt;br&gt;
budget, and drop the software pipeline to one stage. Gate it on pre-Ampere so it is a no-op&lt;br&gt;
on every other card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_device_capability&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_smem_budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;
    &lt;span class="n"&gt;_esz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element_size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_M&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head_size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;_esz&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;_smem_budget&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;TILE_SIZE_PREFILL&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_fits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TILE_SIZE_PREFILL&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;TILE_SIZE_PREFILL&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;TILE_SIZE_DECODE&lt;/span&gt;  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_fits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TILE_SIZE_DECODE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="n"&gt;TILE_SIZE_DECODE&lt;/span&gt;  &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;launch_num_stages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in &lt;code&gt;vllm/v1/attention/ops/triton_unified_attention.py&lt;/code&gt;, graphs capture, the engine&lt;br&gt;
comes up in 76 seconds, and the model serves. &lt;strong&gt;This is not upstream.&lt;/strong&gt; It lives on my&lt;br&gt;
instance and has to be reapplied on any vLLM upgrade, which makes it the obvious thing to&lt;br&gt;
send back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most of the build is kernels that can never load
&lt;/h2&gt;

&lt;p&gt;67 minutes on a &lt;code&gt;g5g.4xlarge&lt;/code&gt; at &lt;code&gt;MAX_JOBS=12&lt;/code&gt;, and the majority of it is FlashAttention.&lt;br&gt;
vLLM compiles FA2 and FA3 &lt;strong&gt;regardless of &lt;code&gt;TORCH_CUDA_ARCH_LIST&lt;/code&gt;&lt;/strong&gt; — I watched it grind&lt;br&gt;
through hundreds of &lt;code&gt;sm90&lt;/code&gt; Hopper instantiations on a build targeting 7.5 only. FA2 needs&lt;br&gt;
sm80, FA3 needs sm90. Neither can ever load on this card.&lt;/p&gt;

&lt;p&gt;Constraining &lt;code&gt;VLLM_FA_CMAKE_GPU_ARCHES&lt;/code&gt; should cut that dramatically. I did not try it,&lt;br&gt;
because by the time I understood what I was looking at the build was 45 minutes in and&lt;br&gt;
interrupting it would have cost more than finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong before I had hardware
&lt;/h2&gt;

&lt;p&gt;I wrote the rig's documentation before provisioning anything. Seven claims in it were wrong,&lt;br&gt;
and every correction came off the machine rather than out of an argument. This is the part I&lt;br&gt;
would keep if I kept nothing else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I wrote&lt;/th&gt;
&lt;th&gt;What the box said&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch aarch64 lacks &lt;code&gt;sm_75&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;AWS DLAMI has it, on both versions I checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bfloat16 is a hard failure here&lt;/td&gt;
&lt;td&gt;Torch upconverts; vLLM logs &lt;code&gt;Casting torch.bfloat16 to torch.float16&lt;/code&gt; and proceeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The backend is XFORMERS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TRITON_ATTN&lt;/code&gt;, forced, not selectable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VLLM_ATTENTION_BACKEND&lt;/code&gt; picks it&lt;/td&gt;
&lt;td&gt;Not a recognised variable. I had shipped dead config.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;w4a16 needs sm80+ Marlin&lt;/td&gt;
&lt;td&gt;The build compiled &lt;code&gt;sm75_kernel_float16_u4b8_float16.cu.o&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The GPU has 16 GB&lt;/td&gt;
&lt;td&gt;15,360 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/v1/completions&lt;/code&gt; returns an empty body&lt;/td&gt;
&lt;td&gt;It returns &lt;code&gt;': ok: ok: ok: ok'&lt;/code&gt; — garbage, not silence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one has teeth. If you health-check by testing for an empty response, this endpoint&lt;br&gt;
passes while producing nonsense. Use &lt;code&gt;/v1/chat/completions&lt;/code&gt; and read the text.&lt;/p&gt;

&lt;p&gt;One claim is still standing only because I never tested it: whether &lt;code&gt;g5g.xlarge&lt;/code&gt;'s 8 GiB of&lt;br&gt;
host RAM can stage 9.5 GiB of weights. Safetensors loading is mmap-backed, so I suspect it&lt;br&gt;
can. It is labelled untested rather than stated as fact, which is where it should have been&lt;br&gt;
all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does once it runs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Site&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Reliability&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Engineering&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(SRE)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;discipline&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;applies&lt;/span&gt;
          &lt;span class="s"&gt;software&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;engineering&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;principles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;infrastructure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;operations&lt;/span&gt;
          &lt;span class="s"&gt;problems&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;highly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reliable,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scalable,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;efficient&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;systems.'&lt;/span&gt;
&lt;span class="na"&gt;finish_reason: stop      usage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;19 prompt / 32 completion / 51 total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Throughput, single stream greedy&lt;/td&gt;
&lt;td&gt;42.9 tok/s @ 64, 43.1 @ 256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache&lt;/td&gt;
&lt;td&gt;2.95 GiB, 329,579 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency at 16k context&lt;/td&gt;
&lt;td&gt;20.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory while serving&lt;/td&gt;
&lt;td&gt;13,501 / 15,360 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine init&lt;/td&gt;
&lt;td&gt;76.4 s, graph capture 17 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory bandwidth, measured&lt;/td&gt;
&lt;td&gt;277.0 GB/s read · 234.3 GB/s copy (320.1 theoretical)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before reading too much into 43 tok/s, note what the memory does. The T4G has &lt;strong&gt;GDDR6, not&lt;br&gt;
HBM&lt;/strong&gt; — 256-bit bus at 5,001 MHz, so 320 GB/s theoretical. I measured &lt;strong&gt;277 GB/s&lt;/strong&gt; on a&lt;br&gt;
streaming read (87% of peak) and 234 GB/s on a read-modify-write. Decode is bandwidth-bound,&lt;br&gt;
so 277 is the real ceiling. For scale, a TPU v5e is about 859 GB/s normalized and a v6e about&lt;br&gt;
1,638 — this part has roughly a third of one and a sixth of the other. It is a bandwidth-limited&lt;br&gt;
card behaving like a bandwidth-limited card.&lt;/p&gt;

&lt;p&gt;Single run, single stream, no repeats and no variance figure. One sample per cell, and taken&lt;br&gt;
with the clamped tiles, so it is a floor rather than a characterisation. My Inferentia port&lt;br&gt;
measured about 44 tok/s for E2B on one core, which is the same neighbourhood — but that is a&lt;br&gt;
different harness on different silicon and I would not put the two in one table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting quick reference
&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;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no kernel image is available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stock arm64 image. No 7.5, no PTX. Build from source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OutOfResources: shared memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turing's 64 KiB against a 512-wide head. Clamp the tiles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AmbiguousGlobalPerLayerAttributeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vLLM older than v0.27.2rc0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;No module named 'setuptools_rust'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing Rust toolchain for &lt;code&gt;vllm-rs&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvcc: not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PyTorch DLAMI has no toolkit. Install &lt;code&gt;cuda-toolkit-13-2&lt;/code&gt; (sbsa).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown vLLM environment variable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You set &lt;code&gt;VLLM_ATTENTION_BACKEND&lt;/code&gt;. It does nothing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Healthy endpoint, nonsense output&lt;/td&gt;
&lt;td&gt;You checked &lt;code&gt;/v1/completions&lt;/code&gt;. Use chat completions.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Take the AWS ARM64 GPU PyTorch DLAMI — it is the only maintained aarch64 stack that still&lt;br&gt;
carries &lt;code&gt;sm_75&lt;/code&gt;. Add &lt;code&gt;cuda-toolkit-13-2&lt;/code&gt; from the sbsa repo and a Rust toolchain, because the&lt;br&gt;
image ships neither. Build vLLM v0.27.2rc0 or newer from source with&lt;br&gt;
&lt;code&gt;TORCH_CUDA_ARCH_LIST=7.5&lt;/code&gt; and &lt;code&gt;use_existing_torch.py&lt;/code&gt;, and patch the Triton attention kernel&lt;br&gt;
to fit Turing's shared memory before you try to start it. Serve with &lt;code&gt;--dtype float16&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;--kv-cache-dtype auto&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing here failed loudly, and nothing failed where I was looking. The packaging gap I built&lt;br&gt;
the rig around was already solved by AWS; the thing that actually stopped me was 32 KiB of&lt;br&gt;
shared memory and a model whose global attention heads are twice as wide as its sliding ones.&lt;br&gt;
Hardware this far off the mainstream will keep producing that shape of surprise — the fix is&lt;br&gt;
not to reason harder about it, but to get to a box sooner and let it tell you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Measured on EC2 &lt;code&gt;g5g.4xlarge&lt;/code&gt; spot, &lt;code&gt;us-east-1a&lt;/code&gt;. NVIDIA T4G, compute capability 7.5,&lt;br&gt;
15,360 MiB, driver 595.71.05. Deep Learning ARM64 AMI OSS Nvidia Driver GPU PyTorch 2.12&lt;br&gt;
(Ubuntu 24.04). torch 2.12.0+cu132, CUDA 13.2. vLLM 0.27.2rc1.dev0+g7f7a32cfe built from&lt;br&gt;
v0.27.2rc0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vllm</category>
      <category>cuda</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Vibecoding EventMatch, built with Antigravity, ADK, and Gemini</title>
      <dc:creator>Sireesha Pulipati</dc:creator>
      <pubDate>Thu, 13 Aug 2026 04:18:51 +0000</pubDate>
      <link>https://dev.to/gde/vibecoding-eventmatch-built-with-antigravity-adk-and-gemini-2gc7</link>
      <guid>https://dev.to/gde/vibecoding-eventmatch-built-with-antigravity-adk-and-gemini-2gc7</guid>
      <description>&lt;p&gt;Every week I get invited to more AI events than I can attend. Bond AI, Founders Bay, AI Collective, half a dozen other newsletters, each one dropping a handful of events into my inbox with no way to tell, at a glance, which ones I’m actually free for. Public aggregators like Luma and Eventbrite don’t help either. The interesting events, the ones from niche community newsletters, never show up there in the first place. If you live in a city like New York or San Francisco, this is a familiar kind of overload, too many good options, scattered across too many places, with no single view of what actually fits your week.&lt;/p&gt;

&lt;p&gt;So I built EventMatch. And I vibecoded it, using Antigravity, Gemini 3.5 Flash, and Google’s Agent Development Kit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sireeshapulipati/event-match" rel="noopener noreferrer"&gt;Code&lt;/a&gt; · &lt;a href="https://www.kaggle.com/competitions/vibecoding-agents-capstone-project/writeups/eventmatch-fight-your-event-fomo" rel="noopener noreferrer"&gt;Kaggle writeup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the walkthrough, what I wanted the app to do, how the agents are put together, and what building it actually taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted the app to do
&lt;/h2&gt;

&lt;p&gt;The core ask was simple to state and harder to deliver. Pull events from public platforms and from the private newsletters sitting in my inbox, check every one of them against my real calendar, and only show me what’s actually conflict free.&lt;/p&gt;

&lt;p&gt;A few things went beyond that baseline. Preferences needed to be conversational, not a set of rigid tags, something like “a mix of tech networking, AI workshops, creative art showcases, and relaxed social gatherings,” not a checkbox list. And those preferences needed to stay stable. If I ask a one off question like “show me free events this weekend,” that shouldn’t silently overwrite my standing preferences. Only an explicit request to update them should.&lt;/p&gt;

&lt;p&gt;The rest was polish that ended up counting for more than I expected. Price shown as a tier, free, , $ , $$$, instead of raw text pulled from the source. One register button per event, not two competing ones in the same modal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foattizkc1w4z3h3nu4z4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foattizkc1w4z3h3nu4z4.png" alt="Event Match UI with top 10 events matching your vibes on the right and a concierge agent on the left" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi agent architecture
&lt;/h2&gt;

&lt;p&gt;EventMatch runs on ADK 2.0, and the workflow alternates between plain Python nodes and LLM agents. That alternation is the whole design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;prepare_extractor (Python)&lt;/strong&gt; sanitizes the incoming message and reads the current session state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;preference_extractor (LlmAgent)&lt;/strong&gt; parses the user’s message to pull out location, and decides whether this is a persistent preference change or a one off search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;filter_events_node (Python)&lt;/strong&gt; pulls public data from Luma and Eventbrite, triggers background fetches from Gmail and Google Calendar, and merges everything. This is where the actual grounding happens. It runs a millisecond level time overlap check against the live calendar and flags conflicts before anything reaches the model, and converts events to plain text descriptions to keep the token count down for the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vibe_ranker (LlmAgent)&lt;/strong&gt; takes the conflict free events and scores them against the user’s preferences, 0 to 100, with a short explanation for each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;formatter_node (Python)&lt;/strong&gt; closes the loop, and it’s the piece that holds the whole system together. The vibe_ranker only ever sees plain text descriptions and returns ranked IDs. formatter_node takes those IDs and re-joins them against the original database records. Registration URLs, host names, event locations, none of it passes through the model twice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkn6daetnrrw7a39iq0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkn6daetnrrw7a39iq0k.png" alt="Deterministic code and probabilistic llm nodes" width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python does the things Python is reliable at, filtering, matching, deduplicating. The LLM does the one thing it’s actually needed for, judging fit against a vague preference. An LLM asked to both evaluate and format a result will, often enough, paraphrase a URL wrong or invent a detail that sounds plausible. Keeping those two jobs separate removes that failure mode instead of trying to prompt it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrations
&lt;/h2&gt;

&lt;p&gt;Underneath the agents is a fairly ordinary web app, FastAPI on the backend, Python 3.11, React on the frontend, built with Vite and Tailwind.&lt;/p&gt;

&lt;p&gt;The integration that makes everything else possible is OAuth. EventMatch requests read only scopes for Gmail and Calendar through Google’s official client libraries. Once authorized, the backend caches the credentials and the UI shows the connected account in the navbar. Everything downstream, the newsletter parsing, the conflict checking, only works because that connection is live.&lt;/p&gt;

&lt;p&gt;Public event data comes from Luma and Eventbrite, which meant working around scraping restrictions rather than clean APIs. Deployment is Docker on Cloud Run, provisioned with Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it, and what it taught me
&lt;/h2&gt;

&lt;p&gt;I built this in Antigravity, following a spec driven process rather than jumping straight into code. Start with a product requirements document. Turn that into a high level design. Turn the design into a spec detailed enough for an agent to build from. Only then does actual development start.&lt;/p&gt;

&lt;p&gt;Coding a working app over a weekend is genuinely exciting. It’s also where that process got tested, and where it broke down. The pattern showed up early and kept showing up. Fix one bug, move to the next, fix that one, and the first bug resurfaces. Back and forth, the same category of issue reappearing in a slightly different shape each time.&lt;/p&gt;

&lt;p&gt;The spec was the actual problem. When the requirements document and the spec are vague about what the app needs to do and what it needs to be tested against, the agent has room to solve the same problem two different ways in two different places, and neither implementation knows about the other. Every fix becomes local. Nothing stays fixed.&lt;/p&gt;

&lt;p&gt;The fix is spending more time upfront, at the PRD stage and the spec stage, reviewing what the agent actually produced there before writing a line of application code. Get the spec and the test plan tight enough that they cover what “done” actually means, and the back and forth during development drops off. Skip that step, and you pay for it later, one resurfacing bug at a time.&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>vibecoding</category>
      <category>antigravity</category>
    </item>
    <item>
      <title>Do I Still Need a Monkey Patch for Gemini Live?</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 13 Aug 2026 01:41:40 +0000</pubDate>
      <link>https://dev.to/gde/do-i-still-need-a-monkey-patch-for-gemini-live-4c3e</link>
      <guid>https://dev.to/gde/do-i-still-need-a-monkey-patch-for-gemini-live-4c3e</guid>
      <description>&lt;p&gt;No. And deleting 187 lines of it was the single biggest benefit of moving to ADK 2.x — but it was not the only one, and it was not the last thing that needed fixing.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Does This Agent Do?
&lt;/h4&gt;

&lt;p&gt;The project is a biometric security scanner, built to exercise the parts of the Gemini Live API that a text chatbot never touches. A browser captures webcam and microphone, streams both to a FastAPI backend over a single WebSocket, and the backend forwards them to Gemini 3.1 Flash Live through the Agent Development Kit. The model watches the video feed, counts the fingers being held up, and calls a tool.&lt;/p&gt;

&lt;p&gt;Three tools are registered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;report_digit(count)&lt;/code&gt; — the detected finger count, which drives the UI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trigger_system_error()&lt;/code&gt; — fired on an offensive gesture, which terminates the session&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trigger_heavy_metal_mode()&lt;/code&gt; — fired on the "Devil's Horns", a secret override&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The transport is deliberately plain. Binary WebSocket frames carry a 1-byte type prefix — &lt;code&gt;1&lt;/code&gt; for audio, &lt;code&gt;2&lt;/code&gt; for JPEG — with 16 kHz PCM going up and 24 kHz PCM coming back, played through an AudioWorklet so the main thread stays free. Everything runs locally with &lt;code&gt;make run&lt;/code&gt;, or on Cloud Run behind &lt;code&gt;make deploy&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;git clone https://github.com/xbill9/way-back-home
cd way-back-home/level_3_new
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two versions live side by side in that repo: &lt;code&gt;level_3&lt;/code&gt;, the original design, and &lt;code&gt;level_3_new&lt;/code&gt;, the current one. Most of this article is the diff between them.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Level 3 Needed to Work
&lt;/h4&gt;

&lt;p&gt;The original build ran on &lt;code&gt;google-adk&lt;/code&gt; 1.27.2, and it worked — but only because a file named &lt;code&gt;patch_adk.py&lt;/code&gt; sat next to it, 187 lines long, applied at import time before anything else could run.&lt;/p&gt;

&lt;p&gt;The problem it solved was real. Gemini 3.1 deprecated &lt;code&gt;media_chunks&lt;/code&gt;, the field a 1.x ADK used to send realtime media. A 1.x ADK talking to a 3.1 Live model would send the deprecated shape and get nothing useful back. The patch monkey-patched three separate call sites to translate:&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;# level_3_gemini/backend/app/patch_adk.py
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rt_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;rt_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[PATCH] Unrolling &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;media_chunks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; from realtime_input.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rt_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_realtime_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_realtime_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three targets were &lt;code&gt;live.AsyncSession.send_realtime_input&lt;/code&gt;, which unrolled &lt;code&gt;media_chunks&lt;/code&gt; into the new typed keywords; &lt;code&gt;GeminiLlmConnection.send_realtime&lt;/code&gt;, which routed each blob to &lt;code&gt;audio=&lt;/code&gt;, &lt;code&gt;video=&lt;/code&gt; or &lt;code&gt;text=&lt;/code&gt; by mime type; and &lt;code&gt;AudioCacheManager.cache_audio&lt;/code&gt;, which was guarded against a &lt;code&gt;NoneType&lt;/code&gt; blob that would otherwise raise.&lt;/p&gt;

&lt;p&gt;It worked, and it was a liability. Monkey patching a framework means every upgrade is a gamble — the patch either becomes redundant, becomes wrong, or silently stops applying because the method it wraps was renamed. The closing recommendation in the original write-up was to delete it the moment the ADK supported the model natively.&lt;/p&gt;

&lt;h4&gt;
  
  
  What ADK 2.x Handles Natively
&lt;/h4&gt;

&lt;p&gt;That moment arrived with &lt;code&gt;google-adk&lt;/code&gt; 2.6.3, and &lt;code&gt;patch_adk.py&lt;/code&gt; was deleted outright. The framework now does the routing itself, detecting the model generation and dispatching on it:&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;# google/adk/models/gemini_llm_connection.py, send_realtime()
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_is_gemini_3_x_live&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_is_gemini_3_5_live_translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;audio/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_gemini_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_realtime_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;image/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_gemini_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_realtime_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Blob not sent. Unknown or empty mime type for&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; send_realtime_input: %s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_gemini_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_realtime_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;media&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the third branch. Audio and image mime types are dispatched explicitly, and anything else is dropped with a warning rather than guessed at — so a blob sent with a missing or unexpected mime type goes nowhere, and the only evidence is a log line.&lt;/p&gt;

&lt;p&gt;Text is handled the same way. A single-part text &lt;code&gt;Content&lt;/code&gt; is routed to &lt;code&gt;send_realtime_input(text=...)&lt;/code&gt; for 3.x models rather than going out as client content, which matches the Live API's own guidance that &lt;code&gt;send_client_content&lt;/code&gt; is only for seeding history.&lt;/p&gt;

&lt;p&gt;That is the whole first patch target and the whole second one, upstream, maintained, and tested by someone else. The third — the &lt;code&gt;NoneType&lt;/code&gt; guard on &lt;code&gt;cache_audio&lt;/code&gt; — was not carried over, because upstream still calls &lt;code&gt;len(audio_blob.data)&lt;/code&gt; unguarded. No path in this application produces a blob with &lt;code&gt;data=None&lt;/code&gt;, so it stays deleted rather than being reintroduced as a precaution.&lt;/p&gt;

&lt;h4&gt;
  
  
  The One Thing That Broke
&lt;/h4&gt;

&lt;p&gt;The patch was hiding a bug in the calling code, and deleting it exposed the bug rather than causing it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LiveRequestQueue.send_realtime()&lt;/code&gt; accepts &lt;code&gt;types.Blob&lt;/code&gt; and nothing else. The old patch had used &lt;code&gt;model_construct&lt;/code&gt; internally, which skips Pydantic validation, so passing a bare string worked by accident. Without the patch it raises a &lt;code&gt;ValidationError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The call site that matters is the keepalive. This project sends a text stimulus every ten seconds when the client goes quiet, and under 1.x that stimulus was a string handed straight to &lt;code&gt;send_realtime()&lt;/code&gt;. Text has to go through &lt;code&gt;send_content()&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_text_stimulus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live_request_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LiveRequestQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;live_request_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&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="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the removal most likely to take an agent off the air quietly. It does not fail at startup. It fails the first time the keepalive fires, ten seconds into a session that otherwise looks healthy. Anyone migrating a Live agent off 1.x should check that call site before touching anything else.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Else Was Updated
&lt;/h4&gt;

&lt;p&gt;The migration was the headline, but it was not the end of the work. Reading the Live API documentation with the source open beside it turned up several things that had been wrong the whole time, none of which any build, test or lint run had ever objected to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video was running at twice the documented maximum.&lt;/strong&gt; The capabilities guide is specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Video frames are sent as individual images (e.g., JPEG or PNG) at a specific frame rate (max 1 frame per second).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The project ran at 2 FPS and permitted up to 5 through an environment variable. Nothing rejects the surplus frames, which is why it went unnoticed — but they are billed, and they consume the session budget twice as fast. &lt;code&gt;VIDEO_FPS&lt;/code&gt; now defaults to 1.0 and is hard-clamped there, so &lt;code&gt;VIDEO_FPS=3&lt;/code&gt; yields 1.0 rather than being honoured. A documented limit that is not enforced is how the 2 FPS crept in to begin with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio-plus-video sessions cap at two minutes.&lt;/strong&gt; From the session management guide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;audio-only sessions are limited to 15 minutes, and audio-video sessions are limited to 2 minutes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Context window compression removes the cap entirely. &lt;code&gt;RunConfig.context_window_compression&lt;/code&gt; defaults to &lt;code&gt;None&lt;/code&gt;, so it has to be asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;context_window_compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ContextWindowCompressionConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sliding_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SlidingWindow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This application streams both continuously, so it had been on the two-minute clock since the first version. Short test sessions never reached it. A demo where someone works through five gestures does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interruptions were documented and unhandled.&lt;/strong&gt; When a user talks over the model, the model stops generating — but the audio it already sent is sitting in the client's ring buffer and keeps playing. The Live API guidance is to stop playback and clear the queue on interruption. ADK surfaces &lt;code&gt;interrupted&lt;/code&gt; on the event, and because the backend forwards whole events as JSON, the flag was already arriving in the browser with nothing reading it. The clearing machinery already existed too. Three lines connected them.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Log Line That Never Ran
&lt;/h4&gt;

&lt;p&gt;Both input and output audio transcription were enabled in &lt;code&gt;RunConfig&lt;/code&gt; from the very first version of this project. Neither ever produced a line of output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;input_transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_audio_transcription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input_transcription&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;input_transcription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&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;USER TRANSCRIPT: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_transcription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_transcript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two mistakes are stacked here. &lt;code&gt;input_audio_transcription&lt;/code&gt; is the &lt;code&gt;RunConfig&lt;/code&gt; field that &lt;em&gt;enables&lt;/em&gt; transcription — it is not the field on the event that transcription produces. And &lt;code&gt;final_transcript&lt;/code&gt; is not a member of &lt;code&gt;types.Transcription&lt;/code&gt; at all; the fields are &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;finished&lt;/code&gt;, &lt;code&gt;language_code&lt;/code&gt;, &lt;code&gt;speaker_label&lt;/code&gt; and &lt;code&gt;words&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either mistake alone raises &lt;code&gt;AttributeError&lt;/code&gt; and gets fixed in minutes. Together, behind the default on &lt;code&gt;getattr&lt;/code&gt;, they produce silence. The condition evaluates to &lt;code&gt;None and ...&lt;/code&gt;, which is falsy, forever.&lt;/p&gt;

&lt;p&gt;The correct field names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;input_transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_transcription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input_transcription&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;input_transcription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&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;USER TRANSCRIPT: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_transcription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gating on &lt;code&gt;finished&lt;/code&gt; is deliberate. ADK emits partial transcription events with &lt;code&gt;finished=False&lt;/code&gt; and one accumulated event with &lt;code&gt;finished=True&lt;/code&gt;, so this produces one clean line per turn instead of one per fragment. The &lt;code&gt;run_live()&lt;/code&gt; docstring is the reference: partial and non-partial events are both yielded to the caller, but only non-partial ones are saved to the session.&lt;/p&gt;

&lt;p&gt;The fix produces this on connect, which is the exact opening line the agent instruction specifies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO - GEMINI TRANSCRIPT: Scanner Online.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Video That Stops When You Look Away
&lt;/h4&gt;

&lt;p&gt;The original design captured frames on a timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;intervalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* capture, send */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rewrite replaced that with &lt;code&gt;requestAnimationFrame&lt;/code&gt; and a manual elapsed-time check. On paper it is the better primitive — frame-aligned, idle when the compositor has nothing to do, and paired with &lt;code&gt;toBlob&lt;/code&gt; instead of &lt;code&gt;toDataURL&lt;/code&gt; it keeps JPEG encoding off the main thread.&lt;/p&gt;

&lt;p&gt;In a backgrounded tab, &lt;code&gt;requestAnimationFrame&lt;/code&gt; is throttled to zero.&lt;/p&gt;

&lt;p&gt;The microphone is not. It runs in an AudioWorklet on the audio thread, which browsers keep alive so capture and playback survive a tab switch. The result is an asymmetric, silent failure: switch tabs and video stops completely while audio streams on. The WebSocket stays open, the session stays billed, and finger detection — the entire point of the application — stops with no error on either side. A 65-second session logged 8,050 audio packets and zero video frames.&lt;/p&gt;

&lt;p&gt;Timers are throttled in background tabs as well, but to roughly one second rather than to zero, so the fix is a self-rescheduling timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;captureFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* capture, send */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;intervalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captureFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frameIntervalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Degrading to roughly 1 FPS beats stopping. Re-reading the interval on each tick also keeps the server's &lt;code&gt;config&lt;/code&gt; frame authoritative, which the rAF version did and a plain &lt;code&gt;setInterval&lt;/code&gt; would not.&lt;/p&gt;

&lt;h4&gt;
  
  
  Code With No Caller
&lt;/h4&gt;

&lt;p&gt;Four things were deleted outright, for ninety lines removed and one added.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;base64 JSON media path&lt;/strong&gt; decoded &lt;code&gt;type: "audio"&lt;/code&gt; and &lt;code&gt;type: "image"&lt;/code&gt; payloads out of JSON. It was not speculative — it was the original design's entire wire protocol, orphaned when media moved to binary frames with a type prefix. Its two tests went with it; they were pinning an implementation, not a contract anyone relied on.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;proactivity&lt;/code&gt; and &lt;code&gt;affective_dialog&lt;/code&gt; query parameters&lt;/strong&gt; were declared on the WebSocket endpoint, documented in the docstring, and read by nothing. In the original design they were real, feeding a conditional &lt;code&gt;RunConfig&lt;/code&gt;. Gemini 3.1 Flash Live then shipped without support for either, the config was removed, and the parameters outlived it. The Live API reference lists both under limitations — "Proactive audio — Not yet supported in Gemini 3.1 Flash Live" and the same line for affective dialogue — each followed by an instruction to remove any configuration for the feature.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;function-response scan&lt;/strong&gt; walked &lt;code&gt;server_content.model_turn.parts&lt;/code&gt; looking for &lt;code&gt;function_response&lt;/code&gt;. &lt;code&gt;model_turn&lt;/code&gt; carries model output; a &lt;code&gt;functionResponse&lt;/code&gt; is something a client sends. The list was structurally always empty.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;second notification channel&lt;/strong&gt;, &lt;code&gt;lastMessage&lt;/code&gt;, was set on every match, system error and heavy-metal trigger, exported from the frontend hook, and read by no component. The callbacks drive the UI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rules for Staying on ADK 2.x
&lt;/h4&gt;

&lt;p&gt;ADK 2.0 moved agents onto a graph engine, and the new constraints fail quietly rather than loudly. Four are worth knowing before writing anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the plain &lt;code&gt;Agent&lt;/code&gt;.&lt;/strong&gt; Custom &lt;code&gt;BaseNode&lt;/code&gt; subclasses and &lt;code&gt;_run_async_impl()&lt;/code&gt; or &lt;code&gt;generate_content()&lt;/code&gt; overrides are &lt;em&gt;silently bypassed&lt;/em&gt;. No error, no warning, no effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hand-append events to the session.&lt;/strong&gt; It circumvents the graph engine and breaks determinism; &lt;code&gt;run_live()&lt;/code&gt; owns the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch broad exception handlers.&lt;/strong&gt; The framework catches exceptions for retries and human-in-the-loop pausing. A broad &lt;code&gt;except&lt;/code&gt; inside a node masks that, and catching &lt;code&gt;BaseException&lt;/code&gt; breaks pausing outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run_live(session=...)&lt;/code&gt; is deprecated.&lt;/strong&gt; Pass &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;session_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project satisfies all four, which is why the upgrade was uneventful: a plain &lt;code&gt;Agent&lt;/code&gt;, &lt;code&gt;InMemorySessionService&lt;/code&gt;, no hand-built events, and its broad handlers sitting in the transport layer rather than inside the graph.&lt;/p&gt;

&lt;p&gt;One 2.x addition is worth adopting. &lt;code&gt;Runner&lt;/code&gt; gained &lt;code&gt;auto_create_session&lt;/code&gt;, and &lt;code&gt;run_live()&lt;/code&gt; already calls its internal get-or-create helper — without the flag a missing session is a &lt;code&gt;ValueError&lt;/code&gt;, which is why the code hand-rolled get-then-create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;APP_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auto_create_session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One is worth skipping. &lt;code&gt;Runner(app=App(...))&lt;/code&gt; is now described as the recommended construction, but &lt;code&gt;Runner(agent=..., app_name=...)&lt;/code&gt; is still supported and gets wrapped into an &lt;code&gt;App&lt;/code&gt; internally, with no deprecation warning. "Recommended" and "required" are different words.&lt;/p&gt;

&lt;p&gt;Whether any of this is still true after the next ADK release is checkable rather than a matter of reading changelogs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -W error::DeprecationWarning -m pytest -q
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero ADK warnings today. That command is the check after any bump.&lt;/p&gt;

&lt;h4&gt;
  
  
  Two Traps Worth Knowing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;ADK's own docstring names a field that does not exist.&lt;/strong&gt; &lt;code&gt;run_live()&lt;/code&gt; refers to &lt;code&gt;RunConfig.save_live_model_audio_to_session&lt;/code&gt;. In 2.6.3 the real fields are &lt;code&gt;save_live_blob&lt;/code&gt; and &lt;code&gt;save_live_audio&lt;/code&gt;. Where documentation and installed source disagree, the source wins — it is the thing that runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A green test suite proves less than it looks like.&lt;/strong&gt; The suites stub &lt;code&gt;run_live()&lt;/code&gt;, which is what makes them hermetic: no API key, no network, no charge. It also means session creation, resumption and teardown are never exercised, and the suite passes whether or not they work. The two changes most capable of breaking every session — &lt;code&gt;auto_create_session&lt;/code&gt; and &lt;code&gt;context_window_compression&lt;/code&gt; — were verified against the real API with a throwaway WebSocket client instead.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Did Not Change
&lt;/h4&gt;

&lt;p&gt;The wire protocol, the agent instruction, the tool definitions and the deployment path all came through the migration untouched: binary frames with a 1-byte prefix, 16 kHz PCM in and 24 kHz out, the three server-side tools, Secret Manager for the API key, and the model-id fallback to &lt;code&gt;gemini-2.5-flash&lt;/code&gt; under &lt;code&gt;adk run&lt;/code&gt;, since the Live preview model still 404s on &lt;code&gt;generateContent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A major-version framework upgrade that touches only the compatibility layer is the outcome to want. It is also the argument for keeping shims isolated in one file with a name that says what it is.&lt;/p&gt;

&lt;h4&gt;
  
  
  So What Really Changed?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;patch_adk.py&lt;/code&gt; is deleted&lt;/strong&gt; — 187 lines and three monkey-patched call sites, replaced by native routing in ADK 2.6.3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text goes through &lt;code&gt;send_content()&lt;/code&gt;&lt;/strong&gt;, because &lt;code&gt;send_realtime()&lt;/code&gt; takes &lt;code&gt;types.Blob&lt;/code&gt; only. The keepalive is the call site that catches people out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video runs at 1 FPS with a hard ceiling&lt;/strong&gt;, matching the documented maximum instead of doubling it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context compression is enabled&lt;/strong&gt;, lifting a two-minute cap on audio-plus-video sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transcript logging works&lt;/strong&gt;, after reading a &lt;code&gt;RunConfig&lt;/code&gt; field name off the event since the original design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The capture loop is a timer again&lt;/strong&gt;, because &lt;code&gt;requestAnimationFrame&lt;/code&gt; stops dead in a background tab while the microphone does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Barge-in clears the playback queue&lt;/strong&gt;, connecting a flag that was already arriving to machinery that already existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ninety lines with no caller are gone&lt;/strong&gt;, along with the two tests that covered them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The Agent Development Kit 2.x release removed the need for a compatibility patch that had been carried since the original build, and deleting it was almost entirely subtractive — the pleasant kind of upgrade. The one removal that bites is &lt;code&gt;send_realtime()&lt;/code&gt; rejecting anything that is not a &lt;code&gt;types.Blob&lt;/code&gt;, which surfaces as a keepalive failure ten seconds into an otherwise healthy session rather than as a crash at startup.&lt;/p&gt;

&lt;p&gt;The wider lesson came after the migration. A framework upgrade tells you what stopped compiling; it says nothing about what still compiles and is wrong. A dead branch compiles. A no-op log line runs. A throttled callback returns cleanly. An undocumented frame rate is accepted by the server. Every one of these survived a migration, a test suite, a lint config and a demo that visibly worked, and each was found by reading the documentation next to the code rather than by running anything.&lt;/p&gt;

&lt;p&gt;For anyone running a Live agent of their own, three checks cost about an hour between them: confirm the transcript handler has ever produced output, confirm video keeps flowing with the tab hidden, and read the session-limit page next to your &lt;code&gt;RunConfig&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>googleadk</category>
      <category>gemini</category>
      <category>geminilive</category>
    </item>
    <item>
      <title>Riverpod to BlocSignal: Incremental Migration and Zero-Codegen Signals for Flutter</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Wed, 12 Aug 2026 23:05:09 +0000</pubDate>
      <link>https://dev.to/gde/riverpod-to-blocsignal-incremental-migration-and-zero-codegen-signals-for-flutter-1gdp</link>
      <guid>https://dev.to/gde/riverpod-to-blocsignal-incremental-migration-and-zero-codegen-signals-for-flutter-1gdp</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: Combining Riverpod Safety with Zero Codegen
&lt;/h2&gt;

&lt;p&gt;If you've been building Flutter apps with &lt;strong&gt;Riverpod&lt;/strong&gt;, you already know the joy of compile-time provider safety, auto-disposal, and synchronous notification propagation via &lt;code&gt;ProviderListenable&lt;/code&gt;. Riverpod fundamentally raised the bar for Flutter state management.&lt;/p&gt;

&lt;p&gt;However, as projects grow, many Riverpod developers run into familiar friction points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Generation Overhead&lt;/strong&gt;: Depending heavily on &lt;code&gt;riverpod_generator&lt;/code&gt; and waiting on &lt;code&gt;build_runner&lt;/code&gt; watches during rapid UI iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex Provider Trees&lt;/strong&gt;: Managing nested &lt;code&gt;ProviderScope&lt;/code&gt; overrides and &lt;code&gt;.family&lt;/code&gt; cache eviction policies when scaling large teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All-or-Nothing Migration Concerns&lt;/strong&gt;: Wanting to trial new reactive primitives or event-driven BLoC architectures without rewriting an entire codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What if you could combine the compile-time safety and synchronous reactivity you love in Riverpod with &lt;strong&gt;zero code generation&lt;/strong&gt;, &lt;strong&gt;fine-grained signal graph reactivity&lt;/strong&gt;, and the ability to &lt;strong&gt;trial or migrate incrementally screen-by-screen&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;BlocSignal&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;bloc_signals_riverpod&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Ergonomics: Riverpod &lt;code&gt;Notifier&lt;/code&gt; vs. &lt;code&gt;CubitSignal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let's compare the classic &lt;strong&gt;Todos&lt;/strong&gt; application—one of the benchmark examples in the official Riverpod monorepo—ported directly to &lt;code&gt;BlocSignal&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Riverpod Approach (Requires &lt;code&gt;@riverpod&lt;/code&gt; &amp;amp; &lt;code&gt;build_runner&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In modern Riverpod, creating a todo list with reactive filtering typically involves a generated &lt;code&gt;Notifier&lt;/code&gt; and separate provider getters or &lt;code&gt;ref.watch&lt;/code&gt; selectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@riverpod&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TodoList&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;_$TodoList&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;completed:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Derived filter provider&lt;/span&gt;
&lt;span class="nd"&gt;@riverpod&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filteredTodos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todoListProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todoFilterProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The BlocSignal Approach (100% Pure Dart 3, No Codegen)
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;BlocSignal&lt;/code&gt;, your state container is a standard handwritten Dart class (&lt;code&gt;CubitSignal&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt;&lt;/code&gt;). Reactive derivations like &lt;code&gt;filteredTodos&lt;/code&gt; and &lt;code&gt;uncompletedCount&lt;/code&gt; are declared inline using &lt;strong&gt;signals &lt;code&gt;computed()&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals/bloc_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:signals_core/signals_core.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TodosCubit&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CubitSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Pass `equals: listEquals` to enforce value-based list equality for state de-duplication!&lt;/span&gt;
  &lt;span class="n"&gt;TodosCubit&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;initialTodos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[]])&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="n"&gt;initialTodos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;equals:&lt;/span&gt; &lt;span class="n"&gt;listEquals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Reactive filter signal&lt;/span&gt;
    &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Synchronously derived computed signals&lt;/span&gt;
    &lt;span class="n"&gt;filteredTodos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;uncompletedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TodoFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ReadonlySignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filteredTodos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ReadonlySignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;uncompletedCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;([..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stateValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;completed:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;setFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TodoFilter&lt;/span&gt; &lt;span class="n"&gt;newFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;filteredTodos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;uncompletedCount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Changed?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;build_runner&lt;/code&gt;&lt;/strong&gt;: No &lt;code&gt;.g.dart&lt;/code&gt; generated files, no background watchers, no build step delays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-In Custom Equality (&lt;code&gt;equals: listEquals&lt;/code&gt;)&lt;/strong&gt;: Because Dart &lt;code&gt;List&lt;/code&gt; instances don't override &lt;code&gt;==&lt;/code&gt; by default, passing &lt;code&gt;equals: listEquals&lt;/code&gt; configures the underlying signal graph to de-duplicate state emissions based on list content equality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-Grained Signal Graph&lt;/strong&gt;: Updates to &lt;code&gt;filter.value&lt;/code&gt; or calling &lt;code&gt;emit(...)&lt;/code&gt; re-evaluate &lt;code&gt;computed()&lt;/code&gt; derivations synchronously and notify only dependent widgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit Container Lifecycles&lt;/strong&gt;: Disposing the cubit disposes its internal signals automatically.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Zero Risk: Incremental Trial &amp;amp; Bidirectional Interop
&lt;/h2&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need to rewrite your application to try &lt;code&gt;BlocSignal&lt;/code&gt;. With the &lt;code&gt;bloc_signals_riverpod&lt;/code&gt; package, you can seamlessly bridge the two frameworks in both directions.&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;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bloc_signals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;bloc_signals_flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;bloc_signals_riverpod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option A: Expose a &lt;code&gt;BlocSignal&lt;/code&gt; / &lt;code&gt;CubitSignal&lt;/code&gt; to Existing Riverpod Widgets
&lt;/h3&gt;

&lt;p&gt;Want to write a new feature or state controller with &lt;code&gt;BlocSignal&lt;/code&gt;, but keep your existing Riverpod UI layer (&lt;code&gt;ConsumerWidget&lt;/code&gt;, &lt;code&gt;WidgetRef&lt;/code&gt;)? &lt;/p&gt;

&lt;p&gt;Simply call &lt;code&gt;.toProvider()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals_riverpod/bloc_signals_riverpod.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create your new BlocSignal or CubitSignal&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todosCubit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TodosCubit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Convert it directly into a Riverpod NotifierProvider!&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todosProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todosCubit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Now consume it anywhere in existing Riverpod widgets:&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyRiverpodWidget&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ConsumerWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LegacyRiverpodWidget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rebuilds reactively whenever todosCubit emits!&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todosProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Adapt an Existing Riverpod Provider into &lt;code&gt;BlocSignal&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Have a legacy Riverpod provider that you need to read from a new &lt;code&gt;BlocSignalBuilder&lt;/code&gt; widget?&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;.toBlocSignal(ref)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals_riverpod/bloc_signals_riverpod.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewFeatureWidget&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ConsumerWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NewFeatureWidget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Adapt any Riverpod ProviderListenable into a BlocSignal container!&lt;/span&gt;
    &lt;span class="c1"&gt;// Automatically binds ref.onDispose to close the container when disposed.&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;todosBloc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;legacyRiverpodProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBlocSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BlocSignalBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlocSignalBase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;,&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;bloc:&lt;/span&gt; &lt;span class="n"&gt;todosBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Converting &lt;code&gt;AsyncValue&lt;/code&gt; &amp;lt;-&amp;gt; &lt;code&gt;AsyncState&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bloc_signals_riverpod&lt;/code&gt; also provides extension methods to map seamlessly between Riverpod's &lt;code&gt;AsyncValue&lt;/code&gt; and Signals' &lt;code&gt;AsyncState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Riverpod AsyncValue to Signals AsyncState&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;asyncState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;riverpodAsyncValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toAsyncState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Signals AsyncState to Riverpod AsyncValue&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;asyncValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signalsAsyncState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toAsyncValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Explore the Official Riverpod Ports on blocsignal.dev
&lt;/h2&gt;

&lt;p&gt;To prove the DX gains and side-by-side equivalence, we've ported canonical state management examples directly from the official &lt;a href="https://github.com/rrousselGit/riverpod/tree/master/examples" rel="noopener noreferrer"&gt;&lt;code&gt;rrousselGit/riverpod&lt;/code&gt;&lt;/a&gt; monorepo into the &lt;code&gt;BlocSignal&lt;/code&gt; open-source repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;📝 &lt;strong&gt;Riverpod Todos&lt;/strong&gt; (&lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/riverpod_todos" rel="noopener noreferrer"&gt;&lt;code&gt;examples/riverpod_todos&lt;/code&gt;&lt;/a&gt;):

&lt;ul&gt;
&lt;li&gt;Replaces &lt;code&gt;@riverpod&lt;/code&gt; codegen and &lt;code&gt;Notifier&lt;/code&gt; with &lt;code&gt;CubitSignal&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt;&lt;/code&gt; and synchronous &lt;code&gt;computed()&lt;/code&gt; signals for reactive filter tabs and stats.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;Pub.dev Package Search&lt;/strong&gt; (&lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/riverpod_pub" rel="noopener noreferrer"&gt;&lt;code&gt;examples/riverpod_pub&lt;/code&gt;&lt;/a&gt;):

&lt;ul&gt;
&lt;li&gt;Replaces Riverpod &lt;code&gt;AsyncNotifier&lt;/code&gt; with &lt;code&gt;BlocSignal&lt;/code&gt; and a streamless &lt;code&gt;restartable()&lt;/code&gt; event transformer that automatically cancels in-flight API requests on keypresses without Rx streams.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🦸 &lt;strong&gt;Marvel Character Browser&lt;/strong&gt; (&lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/riverpod_marvel" rel="noopener noreferrer"&gt;&lt;code&gt;examples/riverpod_marvel&lt;/code&gt;&lt;/a&gt;):

&lt;ul&gt;
&lt;li&gt;Demonstrates API pagination, character search, and widget tree scoping via &lt;code&gt;BlocSignalProvider.value&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Explore all &lt;strong&gt;20 side-by-side benchmark ports&lt;/strong&gt; across BLoC, Signals, and Riverpod live at &lt;strong&gt;&lt;a href="https://blocsignal.dev/#ported-examples" rel="noopener noreferrer"&gt;blocsignal.dev/#ported-examples&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  Built-In AI Agent Skills for Automated Migration
&lt;/h2&gt;

&lt;p&gt;If you use AI coding assistants like &lt;strong&gt;Antigravity&lt;/strong&gt;, &lt;strong&gt;Gemini&lt;/strong&gt;, &lt;strong&gt;Cursor&lt;/strong&gt;, or &lt;strong&gt;GitHub Copilot&lt;/strong&gt;, &lt;code&gt;BlocSignal&lt;/code&gt; publishes a dedicated Agent Plugin skill bundle (&lt;code&gt;riverpod_migration.md&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;When your AI assistant inspects a project with &lt;code&gt;BlocSignal&lt;/code&gt; skills enabled, it automatically understands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to map &lt;code&gt;StateNotifierProvider&lt;/code&gt; / &lt;code&gt;NotifierProvider&lt;/code&gt; to &lt;code&gt;CubitSignal&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;How to preserve auto-disposal and cancellation contracts;&lt;/li&gt;
&lt;li&gt;How to refactor &lt;code&gt;ConsumerWidget&lt;/code&gt; rebuild boundaries to &lt;code&gt;BlocSignalBuilder&lt;/code&gt; or &lt;code&gt;SignalBuilder&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;How to apply &lt;code&gt;bloc_signals_riverpod&lt;/code&gt; interop adapters during multi-phase refactoring.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You don't need to throw away your existing architecture to enjoy the speed, simplicity, and zero-codegen elegance of reactive signals. &lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;&lt;code&gt;bloc_signals_riverpod&lt;/code&gt;&lt;/strong&gt;, you can trial &lt;code&gt;BlocSignal&lt;/code&gt; on a single screen today, bridge your existing Riverpod providers seamlessly, and upgrade your developer experience at your own pace.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website &amp;amp; Comparison Benchmarks&lt;/strong&gt;: &lt;a href="https://blocsignal.dev" rel="noopener noreferrer"&gt;blocsignal.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Pub.dev Packages&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/bloc_signals" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals&lt;/code&gt;&lt;/a&gt; | &lt;a href="https://pub.dev/packages/bloc_signals_flutter" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals_flutter&lt;/code&gt;&lt;/a&gt; | &lt;a href="https://pub.dev/packages/bloc_signals_riverpod" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals_riverpod&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/RandalSchwartz/BlocSignal" rel="noopener noreferrer"&gt;RandalSchwartz/BlocSignal&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>riverpod</category>
      <category>statemanagement</category>
    </item>
    <item>
      <title>MCP Configuration for Looker with Codex</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Wed, 12 Aug 2026 17:06:10 +0000</pubDate>
      <link>https://dev.to/gde/mcp-configuration-for-looker-with-codex-30e1</link>
      <guid>https://dev.to/gde/mcp-configuration-for-looker-with-codex-30e1</guid>
      <description>&lt;p&gt;This article covers the MCP setup and configuration for using Looker with Codex to enhance and extend Looker operations over MCP.&lt;/p&gt;

&lt;h4&gt;
  
  
  Deja Vu — What is Old is New!
&lt;/h4&gt;

&lt;p&gt;This paper is the third pass at the same idea. The original used Gemini CLI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/google-cloud/mcp-configuration-for-looker-with-gemini-cli-55e5671197fb" rel="noopener noreferrer"&gt;MCP Configuration for Looker with Gemini CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;then Antigravity CLI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde/mcp-configuration-for-looker-with-antigravity-cli-504d"&gt;MCP Configuration for Looker with Antigravity CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;then Claude Code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde/mcp-configuration-for-looker-with-claude-code-21jh"&gt;MCP Configuration for Looker with Claude Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this updated version, Codex is used to integrate Looker functionality. The Looker side of the stack does not change at all — that is the whole point of MCP. What changes is the client: how the server gets registered, how tool calls get approved, and where the agent reads its project instructions from.&lt;/p&gt;

&lt;h4&gt;
  
  
  What is Looker?
&lt;/h4&gt;

&lt;p&gt;Looker is a cloud-based business intelligence (BI) and data analytics platform owned by Google Cloud that enables organizations to analyze, visualize, and share data in real-time. It uses a unique modeling language called LookML to define data relationships, offering a centralized “single source of truth” for metrics. Looker focuses on embedded analytics and live data exploration rather than storing data itself.&lt;/p&gt;

&lt;p&gt;More information is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/looker" rel="noopener noreferrer"&gt;Looker business intelligence platform embedded analytics&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Key Features and Capabilities
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;LookML (Looker Modeling Language): A code-based modeling language that allows data analysts to define dimensions, aggregates, and calculations, ensuring consistent metrics across the organization.&lt;/li&gt;
&lt;li&gt;Live Data Connection: Looker does not import data; it queries your data warehouse directly (e.g., BigQuery, Snowflake, Redshift) in real-time, ensuring data is always up to date.&lt;/li&gt;
&lt;li&gt;Embedded Analytics: Looker can be embedded into other applications, websites, or portals, allowing businesses to provide data insights directly within their own tools.&lt;/li&gt;
&lt;li&gt;Self-Service BI: Users can explore data, create visualizations, and build custom dashboards using a browser-based interface without needing deep SQL knowledge.&lt;/li&gt;
&lt;li&gt;Workflow Integration: Actionable data insights can be sent directly to other applications, such as triggering an email based on specific business rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Looker? I thought Big Query Did everything!
&lt;/h4&gt;

&lt;p&gt;Semantic layer is where all the cool kids hang out.&lt;/p&gt;

&lt;h4&gt;
  
  
  What is MCP?
&lt;/h4&gt;

&lt;p&gt;Unless you have been living off grid without Internet- MCP is the new universal connector and next “Big Thing”.&lt;/p&gt;

&lt;p&gt;More information is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/discover/what-is-model-context-protocol" rel="noopener noreferrer"&gt;What is Model Context Protocol (MCP)? A guide&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Google MCP Strategy
&lt;/h4&gt;

&lt;p&gt;Google has gone all-in for all the core Cloud services to provide connections over MCP. An overview is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.cloud.google.com/mcp/overview" rel="noopener noreferrer"&gt;Google Cloud MCP servers overview | Google Cloud Documentation&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  MCP Toolbox
&lt;/h4&gt;

&lt;p&gt;MCP Toolbox is the “swiss army” knife that connects your data sources to MCP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/connect-ide-to-looker-using-mcp-toolbox" rel="noopener noreferrer"&gt;Use Looker with MCP, Gemini CLI and other Agents | Google Cloud Documentation&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Looker MCP Setup
&lt;/h4&gt;

&lt;p&gt;For a more detailed step by step setup instructions — there is a full codelab that goes through the setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codelabs.developers.google.com/codelabs/looker-mcp-toolbox#0" rel="noopener noreferrer"&gt;Connect Gemini CLI to Looker with MCP Toolbox | Google Codelabs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and a further deep dive is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/connect-ide-to-looker-using-mcp-toolbox" rel="noopener noreferrer"&gt;Use Looker with MCP, Gemini CLI and other Agents | Google Cloud Documentation&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Codex
&lt;/h4&gt;

&lt;p&gt;Codex is OpenAI's terminal-driven, agent-assisted coding CLI — the same category of tool as Gemini CLI, Antigravity CLI and Claude Code, and like all of them it ships a full MCP client.&lt;/p&gt;

&lt;p&gt;Install it with npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @openai/codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or with Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then authenticate — Codex will open a browser to sign in with your ChatGPT account, or you can supply an API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the install:&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;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Google Skills Repository
&lt;/h4&gt;

&lt;p&gt;Google Skills give your MCP client well known approaches to work with the core Google products including Big Query.&lt;/p&gt;

&lt;p&gt;The full details are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/level-up-your-agents-announcing-googles-official-skills-repository" rel="noopener noreferrer"&gt;Level Up Your Agents: Announcing Google's Official Skills Repository | Google Cloud Blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To install the Skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills &lt;span class="nb"&gt;install &lt;/span&gt;github.com/google/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This vendors the skills into &lt;code&gt;.agents/skills/&lt;/code&gt; and records them in &lt;code&gt;skills-lock.json&lt;/code&gt;. They are client-neutral markdown, so the same checkout serves Codex, Claude Code and Gemini CLI.&lt;/p&gt;

&lt;h4&gt;
  
  
  What you talkin ‘bout Willis?
&lt;/h4&gt;

&lt;p&gt;That was a lot of setup! But wait- there is more! So what is different about this lab compared to all the others out there?&lt;/p&gt;

&lt;p&gt;This demo is one of the first deep dives into configuring Looker for MCP with Codex. Codex provides a complete working environment with a full MCP client. Looker exposes the key features of the platform over the MCP layer.&lt;/p&gt;

&lt;p&gt;The interesting wrinkle in the Codex version is &lt;strong&gt;approvals&lt;/strong&gt;. Roughly half of the ~50 Looker tools mutate your live instance — &lt;code&gt;make_look&lt;/code&gt;, &lt;code&gt;make_dashboard&lt;/code&gt;, &lt;code&gt;add_dashboard_element&lt;/code&gt;, the &lt;code&gt;*_project_file&lt;/code&gt; family, the git and dev-mode tools. Codex has a first-class per-server approval mode, so this repo pins write tools behind a confirmation prompt while leaving discovery and querying to run freely. Read on.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where do I start?
&lt;/h4&gt;

&lt;p&gt;The strategy for configuring Looker with MCP is an incremental step by step approach.&lt;/p&gt;

&lt;p&gt;First, the Looker configuration settings are retrieved. Then, these settings are used to configure Codex. Finally- Codex is used as a MCP client to the Looker environment. Several samples are run using the Looker MCP Tools directly from Codex.&lt;/p&gt;

&lt;h4&gt;
  
  
  Looker Admin Setup
&lt;/h4&gt;

&lt;p&gt;For Looker (Google Cloud core) — Admins do not directly create keys for standard users; instead, they enable the permission for users to manage their own.&lt;/p&gt;

&lt;p&gt;Navigate to the &lt;a href="https://docs.cloud.google.com/looker/docs/admin-panel-users-users" rel="noopener noreferrer"&gt;Looker Admin Users page&lt;/a&gt; (Admin &amp;gt; Users).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click Edit next to the specific user.&lt;/li&gt;
&lt;li&gt;Locate the API Keys field and toggle it to Enabled.&lt;/li&gt;
&lt;li&gt;Once enabled, the user can generate their own keys by going to their personal &lt;a href="https://docs.cloud.google.com/looker/docs/user-account" rel="noopener noreferrer"&gt;Account settings page&lt;/a&gt; (User Icon &amp;gt; Account &amp;gt; API Keys).&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Looker Instance URL
&lt;/h4&gt;

&lt;p&gt;To connect to the Looker setup — you need to derive your Looker Base URL. Typically this will be the hostname in the Looker app domain.&lt;/p&gt;

&lt;p&gt;For the test instance- this is an example of what the URL looks like (note the HTTPS prefix and no trailing slash):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://780eb09e-7dab-4076-9ec1-ecf9d8414630.looker.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Looker User Setup
&lt;/h4&gt;

&lt;p&gt;First Login to your Looker User environment. Go to Profile-&amp;gt;Account (in upper right hand side) and bring up the user settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vannncuv5cnh5wh4xo3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vannncuv5cnh5wh4xo3.jpeg" width="717" height="825"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the API Key box is unavailable- contact your Admin to enable the API setup on a per user basis.&lt;/p&gt;

&lt;p&gt;Once you have access to create API keys- the settings will look similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxmlue5k8ldi9lpsdy6r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxmlue5k8ldi9lpsdy6r.png" width="628" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click the “Manage” button to setup the API Keys:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouonrmrmv0ck99cu8rwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouonrmrmv0ck99cu8rwm.png" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Create New API key to generate the API Key. Save the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setup the Basic Codex Environment
&lt;/h4&gt;

&lt;p&gt;At this point you should have a working Shell environment and a working Codex installation. All of the relevant code examples and documentation is available in GitHub.&lt;/p&gt;

&lt;p&gt;The next step is to clone the GitHub repository to your local 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;cd&lt;/span&gt; ~
git clone https://github.com/xbill9/looker-mcp-codex
&lt;span class="nb"&gt;cd &lt;/span&gt;looker-mcp-codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;strong&gt;init.sh&lt;/strong&gt; from the cloned directory.&lt;/p&gt;

&lt;p&gt;The script will attempt to determine your shell environment and set the correct variables:&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;source &lt;/span&gt;init.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper script will prompt for your Looker Instance details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xbill@penguin:~/looker-mcp-codex&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;set_env.sh
Looker Base URL &lt;span class="o"&gt;(&lt;/span&gt;e.g. https://your-company.looker.com&lt;span class="o"&gt;)&lt;/span&gt;: https://780eb09e-7dab-4076-9ec1-ecf9d8414630.looker.app
Looker Client ID:
Looker Client Secret:
Downloading MCP Toolbox binary...
Downloading from https://storage.googleapis.com/mcp-toolbox-for-databases/v1.6.0/linux/amd64/toolbox...
  % Total % Received % Xferd Average Speed Time Time Time Current
                                 Dload Upload Total Spent Left Speed
100 292M 100 292M 0 0 71.8M 0 0:00:04 0:00:04 &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- 71.8M
Successfully installed MCP Toolbox binary &lt;span class="o"&gt;(&lt;/span&gt;v1.6.0&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Environment successfully &lt;span class="nb"&gt;set &lt;/span&gt;up.

Current Environment &lt;span class="o"&gt;(&lt;/span&gt;.env&lt;span class="o"&gt;)&lt;/span&gt; — secret masked:
&lt;span class="nv"&gt;GOOGLE_GENAI_USE_VERTEXAI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;True
&lt;span class="nv"&gt;GOOGLE_CLOUD_PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;comglitn
&lt;span class="nv"&gt;GOOGLE_CLOUD_LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;span class="nv"&gt;LOOKER_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://780eb09e-7dab-4076-9ec1-ecf9d8414630.looker.app
&lt;span class="nv"&gt;LOOKER_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;**************&lt;/span&gt;
&lt;span class="nv"&gt;LOOKER_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;********&lt;/span&gt;
&lt;span class="nv"&gt;LOOKER_VERIFY_SSL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_TOOLBOX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/xbill/looker-mcp-codex/toolbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your session times out or you need to re-authenticate- you can run the &lt;strong&gt;set_env.sh&lt;/strong&gt; script to reset your environment variables:&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;source &lt;/span&gt;set_env.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One difference worth calling out versus the Claude Code write-up: with Codex you do &lt;strong&gt;not&lt;/strong&gt; strictly need to &lt;code&gt;source&lt;/code&gt; the script before every session. The launcher reads &lt;code&gt;.env&lt;/code&gt; itself at process start. Sourcing is still the better habit, because it also puts &lt;code&gt;LOOKER_*&lt;/code&gt; into your shell so you can drive &lt;code&gt;toolbox&lt;/code&gt; (or the Looker CLI) by hand.&lt;/p&gt;

&lt;h4&gt;
  
  
  Codex MCP Configuration
&lt;/h4&gt;

&lt;p&gt;Codex reads MCP servers from TOML. This repo ships a project-scoped &lt;code&gt;.codex/config.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers."looker-toolbox"]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"bash"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"./start-looker-mcp.sh"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;cwd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;startup_timeout_sec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;tool_timeout_sec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"writes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four of those lines are the whole story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;command&lt;/code&gt; / &lt;code&gt;args&lt;/code&gt;&lt;/strong&gt; point at a small launcher script rather than at &lt;code&gt;toolbox&lt;/code&gt; directly. That keeps the config file free of both secrets and shell quoting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;startup_timeout_sec = 30&lt;/code&gt;&lt;/strong&gt; — the toolbox binary is ~300 MB and does a real handshake against your Looker instance on boot. The stock timeout is tight enough that a cold start on a slow link can look like a broken server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_timeout_sec = 120&lt;/code&gt;&lt;/strong&gt; — a &lt;code&gt;run_dashboard&lt;/code&gt; against a ten-tile dashboard is ten warehouse queries. Two minutes is a realistic ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt;&lt;/strong&gt; — this is the important one. Discovery and query tools run unattended; anything that mutates the instance stops and asks. See the approvals section below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The launcher, &lt;code&gt;start-looker-mcp.sh&lt;/code&gt;, is deliberately boring:&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;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;

&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.env"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
    &lt;span class="c"&gt;# shellcheck disable=SC1091&lt;/span&gt;
    &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.env"&lt;/span&gt;
    &lt;span class="nb"&gt;set&lt;/span&gt; +a
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_VERIFY_SSL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_VERIFY_SSL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;LOOKER_VERIFY_SSL

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_BASE_URL&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_CLIENT_ID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_CLIENT_SECRET&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"looker-toolbox: Looker credentials are missing. Run: source set_env.sh"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOOKER_TOOLBOX&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;/toolbox&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--stdio&lt;/span&gt; &lt;span class="nt"&gt;--prebuilt&lt;/span&gt; looker,looker-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credential resolution order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; in the project root&lt;/strong&gt; — used when present, and takes precedence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already-exported &lt;code&gt;LOOKER_*&lt;/code&gt; variables&lt;/strong&gt; — used as a fallback when there is no &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither&lt;/strong&gt; — the launcher exits with a message telling you to run &lt;code&gt;source set_env.sh&lt;/code&gt;, rather than dying with an opaque MCP connection error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third case is worth the eight lines it costs. Every MCP client, Codex included, reports a server that exits during startup as a generic transport failure. Failing loudly with a sentence of English turns a twenty-minute debug into a five-second one.&lt;/p&gt;

&lt;p&gt;The repository also retains a Claude-compatible &lt;code&gt;.mcp.json&lt;/code&gt; that launches the same binary through a &lt;code&gt;bash -c&lt;/code&gt; wrapper. Both files contain only variable references, so both are safe to commit.&lt;/p&gt;

&lt;h4&gt;
  
  
  Trusting the Project
&lt;/h4&gt;

&lt;p&gt;Codex will not load a project-scoped config from a directory it does not trust. On first launch inside the repo it will ask; approve it once and the setting sticks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xbill@penguin:~/looker-mcp-codex$ codex

  You are running Codex in ~/looker-mcp-codex

  Since this folder is not version-control trusted, choose how to proceed:

  &amp;gt; 1. Yes, allow Codex to work in this folder
    2. No, exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you would rather register the server globally instead of per-project, put the same &lt;code&gt;[mcp_servers."looker-toolbox"]&lt;/code&gt; block in &lt;code&gt;~/.codex/config.toml&lt;/code&gt; and use an absolute path for &lt;code&gt;command&lt;/code&gt;/&lt;code&gt;cwd&lt;/code&gt;. Project-scoped is the better default here — the launcher, the &lt;code&gt;.env&lt;/code&gt; and the &lt;code&gt;toolbox&lt;/code&gt; binary all live in the checkout, so the config travels with them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Initial Connection
&lt;/h4&gt;

&lt;p&gt;Start Codex from the project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xbill@penguin:~/looker-mcp-codex&lt;span class="nv"&gt;$ &lt;/span&gt;codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use &lt;strong&gt;/mcp&lt;/strong&gt; to confirm the server came up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; /mcp

  MCP Servers

  looker-toolbox   ✔ connected   45 tools
    command  bash ./start-looker-mcp.sh
    cwd      /home/xbill/looker-mcp-codex
    approval writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also check without entering the TUI at all, which is handy in CI or when scripting a machine setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xbill@penguin:~/looker-mcp-codex&lt;span class="nv"&gt;$ &lt;/span&gt;codex mcp list
Name            Command  Args                   Env  Cwd  Status
looker-toolbox  bash     ./start-looker-mcp.sh  -    &lt;span class="nb"&gt;.&lt;/span&gt;    enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and confirm Codex actually parsed every option — a typo'd key is silently ignored, so this is the fast way to prove the approval mode took effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xbill@penguin:~/looker-mcp-codex&lt;span class="nv"&gt;$ &lt;/span&gt;codex mcp get looker-toolbox
looker-toolbox
  enabled: &lt;span class="nb"&gt;true
  &lt;/span&gt;transport: stdio
  &lt;span class="nb"&gt;command&lt;/span&gt;: bash
  args: ./start-looker-mcp.sh
  cwd: &lt;span class="nb"&gt;.&lt;/span&gt;
  &lt;span class="nb"&gt;env&lt;/span&gt;: -
  startup_timeout_sec: 30
  tool_timeout_sec: 120
  default_tools_approval_mode: writes
  remove: codex mcp remove looker-toolbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now ask for the inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; list the looker mcp tools

Here are the Looker MCP tools available in this session, grouped by function:

Metadata / Discovery

- get_models — list LookML models
- get_explores — list explores
- get_dimensions — list dimensions in an explore
- get_measures — list measures in an explore
- get_filters — list filters
- get_parameters — list parameters
- get_dashboards — list dashboards
- get_looks — list Looks
- get_projects — list LookML projects

Querying / Running

- query — run a Looker query
- query_sql — run raw SQL against a connection
- query_url — build a query URL
- run_look — run a saved Look
- run_dashboard — run a dashboard
- run_lookml_tests / get_lookml_tests — LookML data tests

Connections / Database Introspection

- get_connections
- get_connection_databases
- get_connection_schemas
- get_connection_tables
- get_connection_table_columns

Content Creation                                    [approval required]

- make_look — create a Look
- make_dashboard — create a dashboard
- add_dashboard_element — add a tile to a dashboard
- add_dashboard_filter — add a filter to a dashboard
- create_view_from_table — generate a LookML view from a DB table
- generate_embed_url — create an SSO embed URL

LookML Project / File Management                    [approval required]

- get_project_files / get_project_file / create_project_file /
  update_project_file / delete_project_file
- get_project_directories / create_project_directory / delete_project_directory
- validate_project

Git (LookML version control)                        [approval required]

- list_git_branches / get_git_branch / create_git_branch /
  switch_git_branch / delete_git_branch
- dev_mode — toggle development mode

Health / Diagnostics

- health_analyze
- health_pulse
- health_vacuum

That's 51 tools total. The three groups marked [approval required] will prompt
before running, per default_tools_approval_mode = "writes" in .codex/config.toml.

Want me to run one of them (models, connections, or dashboards) to verify connectivity?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  A Word About Approvals
&lt;/h4&gt;

&lt;p&gt;This is where the Codex configuration earns its keep. Codex separates two things that other clients tend to conflate: the &lt;strong&gt;sandbox&lt;/strong&gt; (what the agent may do to your filesystem and network) and &lt;strong&gt;tool approval&lt;/strong&gt; (which MCP tools may fire without a human in the loop).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt; means: run read-only tools freely, prompt before anything that changes state. A &lt;code&gt;make_dashboard&lt;/code&gt; call surfaces as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ⚠ looker-toolbox › make_dashboard  wants to run

    title        VIP Customer Intelligence v2
    description  Real-time insights into top-performing revenue segments
    model        advanced_ecomm

  &amp;gt; 1. Yes, run it
    2. Yes, and don't ask again for make_dashboard
    3. No, and tell Codex what to do differently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option 2 is worth understanding before you reach for it. Approving &lt;code&gt;make_dashboard&lt;/code&gt; for the session is low-risk — it creates new content. Approving &lt;code&gt;delete_project_file&lt;/code&gt; for the session is not. The Looker toolsets do not distinguish create from destroy in their naming, so read the tool name, not the category.&lt;/p&gt;

&lt;p&gt;The blunter instruments still exist if you want them — &lt;code&gt;--ask-for-approval never&lt;/code&gt; for a fully unattended run, &lt;code&gt;--sandbox read-only&lt;/code&gt; to clamp the agent's filesystem access. Neither is a substitute for the per-tool mode: sandbox settings govern the agent's own shell, not what an MCP server does to a remote SaaS instance over HTTPS. A read-only sandbox will happily let a tool call drop a LookML file on your Looker instance.&lt;/p&gt;

&lt;p&gt;The other half of the safety story is &lt;code&gt;AGENTS.md&lt;/code&gt;, which Codex reads automatically from the project root. That is where this repo records the rules the config file cannot express — pass filter values bare, work LookML changes on a dev-mode branch, don't use mutating tools for routine verification, never print the client secret. Same role as &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code and &lt;code&gt;GEMINI.md&lt;/code&gt; for Gemini CLI; all three live side by side in the checkout.&lt;/p&gt;

&lt;h4&gt;
  
  
  All Right Already! Can you actually do something with Looker?
&lt;/h4&gt;

&lt;p&gt;At this point- Codex is set up with the MCP Toolbox and has direct access to all the Looker MCP Tools. Any operations with the Tools come back directly into the Codex context.&lt;/p&gt;

&lt;p&gt;To get an overview of Dashboards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; get_dashboards

• looker-toolbox › get_dashboards

Here are the dashboards in your Looker instance (16 total):

  ID   Title                                       Description
  ──   ─────────────────────────────────────────   ──────────────────────────────────────
   1   2) Business Pulse - Intermediate            —
   2   1) Business Pulse - Basic                   —
   3   3) Business Pulse - Advanced                —
   4   Agreement for Shared Use of this Instance   —
   5   test                                        —
   6   test                                        —
   7   Inventory                                   —
   8   TestJC                                      —
   9   Self-serve industry                         —
  10   TestOrdersByCity                            —
  11   My Dashboard                                —
  12   test-orders                                 —
  13   E-commerce Overview                         Key e-commerce metrics
  14   💎 VIP Customer Intelligence                Advanced LTV and Ranking logic
  15   ☁️ Cloud Optimization Hub                    AI-driven cost savings, Recommender API
  16   Top Users &amp;amp; Countries Overview              Top spenders and high-performing countries

A few observations:
- Several test/scratch dashboards (5, 6, 8, 10, 11, 12) look like leftovers — cleanup candidates.
- The numbered "Business Pulse" set (1–3) are example/tutorial dashboards.
- The substantive content is 13–16.

Want me to run one of these (run_dashboard), or inspect the tiles on a specific one?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also get models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; get_models

• looker-toolbox › get_models

Here are the LookML models in your instance (11 total):

  Model                    Label                 Project                             Connection(s)
  ──────────────────────   ───────────────────   ─────────────────────────────────   ─────────────────────────
  basic_ecomm              Z) Sample LookML      sample_thelook_ecommerce            default_bigquery_connection
  intermediate_ecomm       Z) Sample LookML      sample_thelook_ecommerce            default_bigquery_connection
  advanced_ecomm           Z) Sample LookML      sample_thelook_ecommerce            default_bigquery_connection
  extension-api-explorer   Extension API Expl.   marketplace_extension_api_explorer  default_bigquery_connection
  data_block_acs_bigquery  Census Data Block     marketplace_acs_census-v2           american-community-survey
  gcp_billing_block        Google Cloud Billing  marketplace_gcp-billing             billing-export-sample
  chrome-ux-block          Chrome UX Block       marketplace_chrome-ux-block         chrome-ux
  london_bicycles          London Bicycles       london_bicycles                     london-bicycles
  gde_rk                   Gde Rk                gde_rk                              (8 connections)
  data-dictionary          Data Dictionary       marketplace_extension_data_dict.    default_bigquery_connection
  bq_agent_analytics       Bq Agent Analytics    agent_events                        default_bigquery_connection

Notable groupings:
- theLook e-commerce samples (basic/intermediate/advanced_ecomm) — Looker's standard demo dataset.
- Marketplace blocks — Census/ACS, GCP Billing, Chrome UX, Data Dictionary, API Explorer.
- Public datasets — london_bicycles.
- gde_rk — a multi-connection catch-all model.

Want me to pull the explores for one of these? advanced_ecomm is the richest starting point.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And drill into the details — the &lt;code&gt;advanced_example_ecommerce&lt;/code&gt; explore is where the interesting LookML lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; get_measures for advanced_example_ecommerce

• looker-toolbox › get_measures

20 measures, grouped by view:

order_items — core sales metrics
  order_items.count                 # of Order Items        count
  order_items.total_sale_price      Sales                   sum
  order_items.average_sale_price    Average Price           average
  order_items.first_order_date      First Order Date        date
  order_items.count_for_drill_*     drill-support counts    count

users / products / inventory_items
  users.count                       # of Users              count_distinct
  products.count                    Count Distinct Products count_distinct
  inventory_items.total_cost        Total Cost              sum
  inventory_items.average_cost      Average Cost            average

User-summary measures (filtered vs. lifetime symmetric-aggregate patterns)
  user_summary_filtered.total_sales_for_these_users      Sales (Filtered Summary)   sum_distinct
  user_summary_unfiltered.total_sales_for_these_users    Sales (Lifetime)           sum_distinct
  user_summary_filtered.total_items_for_these_users      Items (Filtered Summary)   sum_distinct
  user_summary_unfiltered.total_items_for_these_users    Items (Lifetime)           sum_distinct

Special measures — advanced ratio/comparison
  user_summary_and_order_items_fields.percent_of_lifetime_unfiltered_sales
  user_summary_and_order_items_fields.filtered_average_vs_unfiltered_average

Notes:
- The filtered vs. lifetime pairs showcase Looker's pattern for comparing a filtered
  subset against a user's full lifetime totals.
- The count_for_*_drill measures exist to support drill-down on dashboard tiles.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's see what VIP tiers look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• looker-toolbox › query

VIP Tiers — Lifetime Revenue Rank Group (advanced_example_ecommerce)

  Rank Group    Users    Total Sales   Order Items   Avg Sale Price
  ───────────   ──────   ───────────   ───────────   ──────────────
  1st 25%       15,569    $4,134,224        53,458           $77.34
  2nd 25%       16,554    $1,931,678        36,005           $53.65
  3rd 25%       16,557      $947,615        24,026           $39.44
  Top 1000 ⭐    1,000      $731,207         4,893          $149.44
  Bottom 25%    16,560      $382,605        17,800           $21.49

What this shows

- Steep revenue concentration. The top quartile drives $4.13M — about 55% of the
  ~$7.4M lifetime total — while the bottom 25% contributes only ~5%, despite having
  more users.
- The "Top 1000" is a distinct elite segment, not a quartile — it overlaps with the
  1st 25%. Just 1,000 users generate $731K, and their $149 average sale price is
  nearly 2× the top quartile's and 7× the bottom's.
- Basket value declines cleanly down the tiers: $77 → $54 → $39 → $21. Rank
  correlates strongly with per-item spend, not just order count.

Reading note: the quartile groups partition all purchasing users, while Top 1000 is
a separate flag sitting inside the 1st quartile — its dollars are also counted in the
1st 25% row. Don't sum the column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The First Approval Prompt
&lt;/h4&gt;

&lt;p&gt;Now build a Look — and this is where the &lt;code&gt;writes&lt;/code&gt; approval mode fires for the first time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; save that as a Look

  ⚠ looker-toolbox › make_look  wants to run

    model     advanced_ecomm
    explore   advanced_example_ecommerce
    title     VIP Tiers — Lifetime Revenue Rank Groups
    fields    user_summary_unfiltered.user_revenue_rank_group,
              users.count, order_items.total_sale_price,
              order_items.count, order_items.average_sale_price
    sorts     order_items.total_sale_price desc
    vis       looker_bar

  &amp;gt; 1. Yes, run it
    2. Yes, and don't ask again for make_look
    3. No, and tell Codex what to do differently

  [1]

• looker-toolbox › make_look

✅ Look created — "VIP Tiers — Lifetime Revenue Rank Groups"

- ID: 8
- URL: /looks/8
- Viz: horizontal bar chart, sorted by total sales, value labels on
- Saved to: your personal folder

Want me to add it to a dashboard, or create companion Looks (Top 1000 by
country/category)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt shows the full argument payload before anything touches the instance. That is the part worth having — you can see &lt;code&gt;explore&lt;/code&gt;, &lt;code&gt;fields&lt;/code&gt; and &lt;code&gt;folder&lt;/code&gt; and catch a wrong model before it creates content, rather than after.&lt;/p&gt;

&lt;h4&gt;
  
  
  But Wait! There’s More!
&lt;/h4&gt;

&lt;p&gt;Once the Tools are exposed by MCP they have full access to Looker resources. Time to update a Dashboard — three mutations, batched into one approval round:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; add it to dashboard 14 and create the companion Looks

I'll do all three — add the tile to dashboard 14, and create both companion Looks.

  ⚠ looker-toolbox › add_dashboard_element  →  dashboard_id 14      [1] approved
  ⚠ looker-toolbox › make_look  →  "Top 1000 VIPs by Country"       [1] approved
  ⚠ looker-toolbox › make_look  →  "Top 1000 VIPs by Category"      [1] approved

• looker-toolbox › (3 calls)

All three succeeded. Pulling the underlying data so you can see what they contain.

• looker-toolbox › (2 calls)

✅ Dashboard tile added
"VIP Tiers — Lifetime Revenue Rank Groups" is now a tile on 💎 VIP Customer
Intelligence (dashboard 14).

✅ Look 9 — Top 1000 VIPs by Country (/looks/9)

  Country          VIP Users   Total Sales   Avg Price
  ──────────────   ─────────   ───────────   ─────────
  China                  359      $259,129     $147.23
  United States          224      $163,185     $147.41
  Brasil                 143      $106,613     $155.64
  South Korea             53       $37,381     $129.34
  France                  52       $37,178     $149.31
  Spain                   47       $35,646     $163.51
  Germany                 43       $32,112     $158.19
  United Kingdom          37       $28,494     $178.09
  …6 more (AU, BE, JP, PL)

✅ Look 10 — Top 1000 VIPs by Product Category (/looks/10)

  Category              Total Sales   Items   Avg Price
  ───────────────────   ───────────   █████   ─────────
  Outerwear &amp;amp; Coats        $245,412     664     $369.60
  Suits &amp;amp; Sport Coats       $70,838     278     $254.81
  Jeans                     $58,421     423     $138.11
  Active                    $48,295     220     $219.52
  Sweaters                  $36,618     325     $112.67
  Dresses                   $35,942     185     $194.28
  …9 more

Notable insights
- Geography is concentrated: China + US + Brasil = ~726 of the 1,000 VIPs. The UK
  has the highest per-item spend ($178) despite few users.
- Outerwear dominates category spend — $245K, more than the next three categories
  combined, at a $369.60 average sale price. High-ticket outerwear is the signature
  purchase of your best customers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will look similar to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonktw9tdel1qhqkr349w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonktw9tdel1qhqkr349w.png" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And can also be accessed directly in Looker:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkuy0lw8nwqp5lb2t5l1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkuy0lw8nwqp5lb2t5l1v.png" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  So what else can Codex do?
&lt;/h4&gt;

&lt;p&gt;Health checks and instance maintenance — and the first real one surfaced a finding, just not the expected kind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; run a full health sweep

Running usage analysis, six pulse checks, and cleanup candidates in parallel.

• looker-toolbox › (10 calls)


Meanwhile — a metadata-based audit does work

get_dashboards / run_dashboard don't touch System Activity, so here's a cleanup pass
from what we already gathered:

  ID     Title                          Flag
  ────   ────────────────────────────   ─────────────────────────────────────
  5, 6   "test" (×2, duplicate name)    Scratch — almost certainly deletable
  8      TestJC                         Scratch
  10     TestOrdersByCity               Scratch
  11     My Dashboard                   Default/placeholder name
  12     test-orders                    Scratch
  14     💎 VIP Customer Intelligence   Superseded by v2 (#17) — 7 broken tiles

~7 dashboards worth reviewing for deletion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what Codex did &lt;em&gt;not&lt;/em&gt; do here: it identified six deletion candidates and stopped. Deleting dashboards is a mutation, and the approval mode plus the &lt;code&gt;AGENTS.md&lt;/code&gt; rule ("confirm before creating/altering/deleting Looker content") both point the same direction. The agent proposes; you dispose.&lt;/p&gt;

&lt;h4&gt;
  
  
  Show me the Money!
&lt;/h4&gt;

&lt;p&gt;The key value shows up when you push the combination of Codex, the model, and the Looker tools over MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; "Are we at risk of losing our best customers?"

I'll investigate using your Looker instance. Starting with what data is available,
then digging into retention signals.

• looker-toolbox › get_models

This is the thelook e-commerce dataset. Exploring the richest model.

• looker-toolbox › (3 calls)

Rich model — there's a lifetime revenue-rank on each user, so I can define "best
customers" precisely (Top Revenue Group / Top 1000) and track activity over time.
First, the data's date range, so I know what "recent" means.

• looker-toolbox › query

Data runs right up to today. Now the core analysis: the Top Revenue Group's monthly
spend and active-customer count over the last ~2 years.

• looker-toolbox › query

The business is growing overall, so a raw trend hides churn. The real risk signal is
recency — how many of our best customers have gone quiet.

• looker-toolbox › (4 calls)

Here's what the data says. I defined "best customers" as your Top Revenue Group —
the top 1,000 by lifetime valid revenue ($731K lifetime, ~$731 each). Then I looked
at whether they're still actually buying.

Short answer: the cohort's revenue is healthy, but there's a large dormant tail.

Aggregate spend from top customers is at record highs — valid revenue rose from
~$12–17K/month in late 2024 to $43K most recently, growing alongside the overall
business. The franchise isn't collapsing.

But recency tells a more worrying story. Of the 1,000 all-time best customers:

  Last purchased within   # still active   Share   → Gone silent
  ─────────────────────   ──────────────   ─────   ───────────────────────────
  90 days                            258     26%   74% (742) inactive 3+ months
  180 days                           419     42%   58% (581) inactive 6+ months
  365 days                           585     59%   42% (415) inactive 12+ months

So ~415 of your top-1,000 (42%) haven't placed a valid order in over a year —
effectively lapsed. At the cohort's average lifetime value (~$731), that's on the
order of ~$300K of historical revenue that has stopped repeating, plus another ~160
in the 6–12 month "cooling off" zone.

How to read this

- It's a tail problem, not a top-line collapse. A concentrated set of top customers
  is very active, while a long tail of former big spenders has quietly aged out.
  That's partly natural — "best customers" is a lifetime label.
- The actionable risk is the 6–12 month cooling band (~160 customers) — recently
  valuable, now slipping, still winnable.

Suggested next steps (I can do any of these — just say the word)

1. Pull the actual at-risk list — name, email, lifetime spend, days-since-last-order.
2. Build a "Best Customer Retention" dashboard so this is monitored ongoing.
3. Add a churn-risk segment to the LookML model so "at-risk best customers" becomes
   a reusable, governed field.

Note that #2 and #3 create or modify content in your live Looker instance — those
will come through as approval prompts, and I'll confirm specifics before running them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull at-risk customers in real time, based on your actual data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your best customers who've gone quiet

The 5 highest-lifetime-value members of the Top-1,000 revenue group with no valid
order in 90+ days, ranked by lifetime spend. "Cooling" = last bought 3–6 months ago
(most winnable); "Dormant" = 6+ months silent.

  #  Customer          Country    Channel    Lifetime $  Orders  Status
  ─  ───────────────   ────────   ────────   ──────────  ──────  ──────────
  1  Craig Dickerson   France     Email          $1,849       8  🟡 Cooling
  2  Mark Nolan        USA        Organic        $1,826       7  🟡 Cooling
  3  John Hill         Brazil     Search         $1,554       4  🔴 Dormant
  4  Glenn Foster      USA        Facebook       $1,455       8  🟡 Cooling
  5  Thomas Davis      Australia  Search         $1,435       8  🔴 Dormant

Notable: your single highest-lifetime-value customer overall (Craig Dickerson,
$1,849) is on this list — cooling, not yet lost.

The full at-risk population (all 1,000 top customers)

- 🟢 258 active (bought in last 90 days) — healthy
- 🟡 161 cooling — last order 3–6 months ago  ← highest-priority win-back
- 🟠 166 slipping — last order 6–12 months ago
- 🔴 415 lapsed — no order in 12+ months

So 742 of your top 1,000 (74%) haven't purchased in 90+ days.

Method note: ranked by lifetime valid spend; "at-risk" = no valid order in 90 days,
cross-checked against the 90- and 180-day active sets. Status is bucketed by
90/180-day windows rather than exact per-customer last-order dates — the toolbox
couldn't compute that inline.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What-if scenarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; "What if we raised outerwear prices 10% — what happens to VIP revenue and would it
   dent volume?"

A pricing what-if on the VIP segment. Grounding it in actual numbers first.

• looker-toolbox › (2 calls)

Outerwear is the key finding: it's your VIPs' #1 category by a wide margin — $245K
(34% of all VIP lifetime revenue), 664 items at $370/item. This decision hits VIPs
where they're most concentrated. Pulling cost too, for profit impact.

Baseline — VIP outerwear today

  Metric             Value
  ────────────────   ───────────────────────
  Revenue            $245,412
  Units sold         664
  Avg price / unit   $369.60
  Cost / unit        $163.48
  Gross profit       $136,859 (55.8% margin)
  Profit / unit      $206.11

What a +10% price does

New price = $406.56 (+$36.96/unit; cost unchanged, so every extra dollar is margin).

  Volume response      Units (Δ)     Revenue   vs today   Gross profit   vs today
  ──────────────────   ──────────   ────────   ────────   ────────────   ────────
  No dip (inelastic)   664  (0%)    $269,956   +$24,544       $161,405   +$24,546
  −5%                  631 (−33)    $256,458   +$11,046       $153,335   +$16,476
  −10%                 598 (−66)    $242,960    −$2,452       $145,266    +$8,407
  −15%                 564 (−100)   $229,462   −$15,950       $137,197      ~flat
  −20%                 531 (−133)   $215,964   −$29,449       $129,127    −$7,732

The two numbers that matter

- Revenue break-even: ~9% volume loss. You can lose up to ~60 of the 664 units
  before the hike stops adding revenue.
- Profit break-even: ~15% volume loss. Because you keep the full $37 markup at zero
  added cost, profit holds even if revenue dips.

Would it dent volume? Expect a modest dip. For your most loyal customers buying a
premium considered-purchase category, elasticity is likely mild (roughly −0.3 to
−0.8) — the top two rows: revenue +$11–20K, profit +$16–22K, only ~20–50 fewer units.

One strategic caveat

Tying back to the last analysis: 74% of your VIPs are already cooling or lapsed, and
outerwear is their anchor category. A blanket 10% hike risks nudging cooling VIPs out
the door, and that churned lifetime value would swamp the ~$15–20K modeled gain.
- Raise on healthy/active VIPs; protect the at-risk ones.
- Apply to new-season inventory rather than a visible retroactive hike.
- Run it as a holdout test on a VIP subset first to get real elasticity.

Caveat on the model: elasticity is assumed, not measured — no price-experiment
history exists in this data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Reading the LookML
&lt;/h4&gt;

&lt;p&gt;Because &lt;code&gt;looker-dev&lt;/code&gt; is loaded alongside &lt;code&gt;looker&lt;/code&gt;, Codex can read the actual model source, not just the metadata — which is what makes "can I trust this measure?" an answerable question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; "What does filtered_average_vs_unfiltered_average actually compute, and can I
   trust it?"

Let me read the LookML that defines it. First, locating the project files.

• looker-toolbox › (2 calls)

The measure lives in the user_summary_and_order_items_fields view. Reading it plus
the two views it depends on.

• looker-toolbox › (3 calls)

I have the full definition and its dependencies. Verifying my reading against live
data before giving a verdict — pulling the raw ingredients alongside the measure.

• looker-toolbox › query

The math reconciles exactly (Outerwear: 145.37 / 88.47 − 1 = 0.643 ✓). Full teardown:

What it computes

The percent difference between the average item price in the current query context
and the same shoppers' average item price over their entire lifetime. Positive =
they're buying pricier-than-usual items in this slice.

The actual SQL

  measure: filtered_average_vs_unfiltered_average {
    label: "Avg Price vs These Users' Avg Price (Lifetime)"
    type: number
    sql: (${order_items.average_sale_price} * 1.0
          / nullif(${user_summary_unfiltered.average_sale_price_for_these_users}, 0)) - 1 ;;
    value_format_name: percent_0
  }

Where each half comes from — the clever part

  Term          Field                                    Respects query filters?
  ───────────   ──────────────────────────────────────   ─────────────────────────
  Numerator     order_items.average_sale_price           Yes — rows in scope
  Denominator   user_summary_unfiltered.average_sale_    No — lifetime avg for
                price_for_these_users                    those same users

The denominator's "lifetime" behavior comes from one line:

  derived_table: {
    explore_source: advanced_example_ecommerce { ... bind_all_filters: no }
  }

That derived table is one row per user, joined back to order_items by user_id.
Because it's a 1-user-row joined to many order-item rows, Looker applies symmetric
aggregates so summing lifetime sales/items counts each user exactly once (no
fan-out). Its twin, user_summary_filtered, is identical but bind_all_filters: yes.

Empirically verified:

  Category            Filtered avg   Lifetime avg   Measure   Check
  ─────────────────   ────────────   ────────────   ───────   ──────────────────────
  Outerwear &amp;amp; Coats        $145.37         $88.47      +64%   145.37/88.47−1 = 0.643 ✓
  Suits                    $118.39         $77.38      +53%   ✓
  Blazers &amp;amp; Jackets         $92.51         $67.84      +36%   ✓

Can you trust it? ✅ Yes — with three rules

Correctly built: proper symmetric-aggregate handling, nullif(...,0) guards against
divide-by-zero, both sides on the same "Valid Orders only" basis. No bug here.

But it's a ratio-of-averages, so respect its nature:

1. Don't sum, average, or trust a Totals row for it. Ratio measures don't roll up.
2. The baseline is dynamic, not a global constant. Change a filter that changes which
   users appear and the baseline moves. This is the #1 way people misread it.
3. It's about per-item price mix, not spend or loyalty.

Bottom line: trustworthy for exactly one question — "in this slice, are these
shoppers trading up or down on per-item price versus their own norm?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that this whole exchange used only read tools — &lt;code&gt;get_project_files&lt;/code&gt;, &lt;code&gt;get_project_file&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt; — so it ran without a single approval prompt. The mode is calibrated so that &lt;em&gt;understanding&lt;/em&gt; your instance is frictionless and &lt;em&gt;changing&lt;/em&gt; it is not.&lt;/p&gt;

&lt;h4&gt;
  
  
  Executive briefings
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The dashboard existed already (dashboard 14, built earlier from advanced_ecomm). To
make it a proper briefing I added an executive layer on top of the existing charts:
a 4-KPI summary strip, and a "bottom line" callout.

The briefing in 30 seconds

  Metric                  Value    Meaning
  ─────────────────────   ──────   ─────────────────────────────────────
  Lifetime revenue        $7.40M   All purchasing users
  Top-1000 VIP revenue    $731K    $149 avg basket — ~7× the bottom tier
  Revenue concentration   56%      From the 1st customer quartile
  VIP geography           72%      Of VIP spend from China, US, Brasil

Bottom line for the exec team: revenue is highly concentrated — a 1,000-customer VIP
core spends ~7× the average shopper, is geographically narrow (three countries), and
is premium-led (Outerwear &amp;amp; Coats alone: $245K at a $370 avg item). The strategic
lever is retaining and expanding the VIP core, not broad acquisition.

One note on reading the charts: the Top 1000 tier is an elite flag inside the 1st
quartile — its dollars are already counted there, so don't sum the tier bars.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxh4n24bf7wr13cydgl8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxh4n24bf7wr13cydgl8i.png" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttz014h1eyprz3xjprxn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttz014h1eyprz3xjprxn.png" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are only high level examples of what can be done with the tech stack!&lt;/p&gt;

&lt;h4&gt;
  
  
  One More Thing: the Looker CLI
&lt;/h4&gt;

&lt;p&gt;The MCP path is not the only way in. The repo also ships a &lt;code&gt;Makefile&lt;/code&gt; target that installs the &lt;a href="https://github.com/looker-open-source/looker-cli" rel="noopener noreferrer"&gt;Looker CLI&lt;/a&gt; into the project root, checksum-verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make cli                              &lt;span class="c"&gt;# latest release&lt;/span&gt;
make cli &lt;span class="nv"&gt;LOOKER_CLI_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v0.4.8    &lt;span class="c"&gt;# pinned&lt;/span&gt;
make clean                            &lt;span class="c"&gt;# remove downloaded binaries, keep credentials&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the same &lt;code&gt;LOOKER_*&lt;/code&gt; variables, so &lt;code&gt;source set_env.sh&lt;/code&gt; covers both. Useful for the deterministic, scriptable half of the work — CI checks, bulk operations — while MCP covers the exploratory half.&lt;/p&gt;

&lt;h4&gt;
  
  
  Troubleshooting
&lt;/h4&gt;

&lt;p&gt;A short list of the things that actually go wrong:&lt;/p&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;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;
&lt;code&gt;/mcp&lt;/code&gt; shows no servers&lt;/td&gt;
&lt;td&gt;Project not trusted, so &lt;code&gt;.codex/config.toml&lt;/code&gt; never loaded&lt;/td&gt;
&lt;td&gt;Restart &lt;code&gt;codex&lt;/code&gt; in the repo root and approve the trust prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server fails immediately&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;.env&lt;/code&gt; and no exported &lt;code&gt;LOOKER_*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;source set_env.sh&lt;/code&gt; — the launcher prints exactly this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server times out on startup&lt;/td&gt;
&lt;td&gt;300 MB binary + Looker handshake on a cold/slow start&lt;/td&gt;
&lt;td&gt;Raise &lt;code&gt;startup_timeout_sec&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;run_dashboard&lt;/code&gt; times out&lt;/td&gt;
&lt;td&gt;Ten tiles = ten warehouse queries&lt;/td&gt;
&lt;td&gt;Raise &lt;code&gt;tool_timeout_sec&lt;/code&gt;, or run tiles individually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All &lt;code&gt;health_*&lt;/code&gt; return Access Denied&lt;/td&gt;
&lt;td&gt;API3 role lacks &lt;code&gt;see_system_activity&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Admin → Roles, add the permission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter returns nothing&lt;/td&gt;
&lt;td&gt;Value was quoted&lt;/td&gt;
&lt;td&gt;Pass values bare — &lt;code&gt;first_touch&lt;/code&gt;, not &lt;code&gt;"first_touch"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools run without asking&lt;/td&gt;
&lt;td&gt;Approval mode not applied&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;codex mcp get looker-toolbox&lt;/code&gt; — if the key isn't echoed back it was misspelled and silently dropped. Also check you didn't pick "don't ask again" earlier in the session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;Codex was configured as a Looker MCP client using the MCP Toolbox. The &lt;code&gt;.codex/config.toml&lt;/code&gt; registration points at a secret-free launcher script that resolves credentials from &lt;code&gt;.env&lt;/code&gt; at runtime, and pins write-capable tools behind &lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt; so discovery and analysis run unattended while anything that mutates the live instance stops and asks. The MCP connection was then used to explore the instance, read and verify LookML, build Looks and dashboards, and run open-ended business analysis against the governed semantic model.&lt;/p&gt;

&lt;p&gt;The stack underneath is unchanged from the Gemini CLI, Antigravity CLI and Claude Code versions of this paper — same &lt;code&gt;toolbox&lt;/code&gt; binary, same &lt;code&gt;looker,looker-dev&lt;/code&gt; toolsets, same ~50 tools. That is the actual result worth noting: the same repository, with three client config files sitting side by side, serves all of them.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>codex</category>
      <category>lookml</category>
      <category>cli</category>
    </item>
    <item>
      <title>Preventing Quota Crashes via Antigravity CLI Agent Hooks</title>
      <dc:creator>Tanaike</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:15:38 +0000</pubDate>
      <link>https://dev.to/gde/preventing-quota-crashes-via-antigravity-cli-agent-hooks-24hd</link>
      <guid>https://dev.to/gde/preventing-quota-crashes-via-antigravity-cli-agent-hooks-24hd</guid>
      <description>&lt;h2&gt;
  
  
  Solving the LLM quota monitoring paradox with zero-overhead local Connect RPC agent hooks.
&lt;/h2&gt;




&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;Google Antigravity CLI users using Google OAuth face abrupt task failures when API quota hits 0%, while account switching triggers unrecoverable signature errors. Querying quota via LLM tool calls creates a paradox by consuming the very tokens being monitored. We resolve this with &lt;code&gt;antigravity-cli-check-usage-plugin&lt;/code&gt;, a CLI Agent Hook running outside the LLM execution turn. Directly querying local Connect RPC endpoints, it monitors quota with zero token overhead and injects proactive warning banners when threshold limits are reached.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Developers relying on &lt;strong&gt;Google Antigravity CLI&lt;/strong&gt; for autonomous pair programming frequently encounter a frustrating barrier: running out of API quota mid-session. When using Google OAuth authentication, your quota can silently hit 0%, causing task execution to halt abruptly with an unrecoverable quota error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠ Individual quota reached. Please upgrade your subscription to increase your limits. Resets in 1h00m00s.
Error ID: 49a81c0f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To bypass this roadblock, developers often attempt to log out and switch to a paid Google Cloud project billing account. However, in &lt;strong&gt;Antigravity CLI v1.1.12&lt;/strong&gt;, attempting to resume an active agent session after switching accounts triggers a critical signature mismatch failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠ Invalid thought signature.
Error ID: e2901f4c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error prevents the session from continuing, forcing you to wait until the quota resets. While future CLI updates may resolve this session state issue, waiting for a patch is not a viable strategy when shipping code today.&lt;/p&gt;

&lt;p&gt;The architectural divergence between standard tool-based monitoring and our agent hook model is illustrated in Figure 1. While developers can manually run the &lt;code&gt;/usage&lt;/code&gt; slash command to view quota, &lt;strong&gt;AI agents executing multi-step autonomous tasks cannot trigger &lt;code&gt;/usage&lt;/code&gt; programmatically&lt;/strong&gt;. In traditional CLI workflows, invoking quota checks via LLM tool calls requires passing context back and forth through the inference API, depleting active model tokens. Conversely, the zero-overhead agent hook interceptor executes locally prior to prompt dispatch, querying the process socket silently and injecting status alerts only when remaining quota breaches configured safety bounds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rot60e9sqw34vxdspd4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rot60e9sqw34vxdspd4.jpg" alt="Figure 1: Architectural comparison between traditional CLI agent quota limitations and the zero-overhead agent hook workflow." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, to overcome the limitation of agents being unable to trigger &lt;code&gt;/usage&lt;/code&gt;, we walk through the engineering journey of building &lt;strong&gt;&lt;code&gt;antigravity-cli-check-usage-plugin&lt;/code&gt;&lt;/strong&gt;. By combining local Connect RPC inspection with proactive lifecycle hooks, this plugin automatically performs external quota checks with &lt;strong&gt;Zero Quota Consumption (0 LLM tokens)&lt;/strong&gt;, completely preventing mid-session crashes.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Repository
&lt;/h2&gt;

&lt;p&gt;The plugin developed and discussed in this article is open-sourced and available on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/tanaikech/antigravity-cli-check-usage-plugin" rel="noopener noreferrer"&gt;tanaikech/antigravity-cli-check-usage-plugin&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repository contains the dual-runner entrypoint (&lt;code&gt;entrypoint.sh&lt;/code&gt;), Python script (&lt;code&gt;check_quota.py&lt;/code&gt;), pure Bash fallback script (&lt;code&gt;check_quota.sh&lt;/code&gt;), lifecycle hook manifest (&lt;code&gt;hooks.json&lt;/code&gt;), and default threshold configuration (&lt;code&gt;config.json&lt;/code&gt;), allowing instant one-command installation as an Antigravity CLI plugin across any developer environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Core Motivation
&lt;/h2&gt;

&lt;p&gt;While Antigravity CLI provides the &lt;code&gt;/usage&lt;/code&gt; slash command for developers to manually inspect quota limits, AI agents executing autonomous task loops cannot invoke &lt;code&gt;/usage&lt;/code&gt; programmatically.&lt;/p&gt;

&lt;p&gt;If we attempted to solve this by equipping the AI agent with a custom tool to query the internal RPC endpoint (&lt;code&gt;/exa.language_server_pb.LanguageServerService/GetUserStatus&lt;/code&gt;), the tool invocation and context turns would consume LLM API tokens. This creates a fundamental paradox: &lt;strong&gt;using LLM context tokens to check remaining quota consumes the very quota you are trying to preserve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addressing this challenge, the solution built upon our previously published article, &lt;a href="https://medium.com/google-cloud/a-developers-guide-to-agent-hooks-in-antigravity-cli-4c1440febd11" rel="noopener noreferrer"&gt;A Developer’s Guide to Agent Hooks in Antigravity CLI&lt;/a&gt;. Recalling the out-of-band execution mechanics of CLI Agent Hooks explored in that guide, we leveraged lifecycle events (&lt;code&gt;PreInvocation&lt;/code&gt; and &lt;code&gt;PostInvocation&lt;/code&gt;) to run local process checks completely outside the LLM inference turn—guaranteeing zero API token quota overhead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Token Overhead&lt;/strong&gt;: During normal operation, quota checking runs entirely outside the LLM context (via local Python/Bash scripts) without invoking LLM tool calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local RPC Interception&lt;/strong&gt;: It automatically queries the CLI's internal status endpoint on &lt;code&gt;127.0.0.1&lt;/code&gt; without external network calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proactive Threshold Alerting&lt;/strong&gt;: It notifies both the developer and the AI agent &lt;em&gt;before&lt;/em&gt; quota hits 0%, preventing session corruption and hard crashes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Connect RPC
&lt;/h2&gt;

&lt;p&gt;Through reverse-engineering the Antigravity CLI local process architecture (originally explored in the &lt;a href="https://github.com/skainguyen1412/antigravity-usage" rel="noopener noreferrer"&gt;antigravity-usage repository by skainguyen1412&lt;/a&gt;), we discovered that the running &lt;code&gt;agy&lt;/code&gt; process hosts a local HTTPS server using the gRPC / Connect Protocol on &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By querying the internal endpoint &lt;code&gt;/exa.language_server_pb.LanguageServerService/GetUserStatus&lt;/code&gt;, we can retrieve real-time model quota fractions and reset timestamps directly from the local process.&lt;/p&gt;

&lt;p&gt;Because the &lt;code&gt;agy&lt;/code&gt; process may open multiple listening sockets on &lt;code&gt;127.0.0.1&lt;/code&gt; for IPC and WebSockets, a shell loop that probes each detected port until it receives a valid &lt;code&gt;userStatus&lt;/code&gt; response is required:&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;# Scan listening sockets for the active 'agy' process on loopback (127.0.0.1)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;PORT &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;ss &lt;span class="nt"&gt;-tulpn&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;agy | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'127.0.0.1:'&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# Post a Connect Protocol request to the internal GetUserStatus RPC endpoint&lt;/span&gt;
  &lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://127.0.0.1:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/exa.language_server_pb.LanguageServerService/GetUserStatus &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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Connect-Protocol-Version: 1"&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;'{"metadata":{"ideName":"antigravity","extensionName":"antigravity","locale":"en"}}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# Verify if the response contains the userStatus JSON key&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"userStatus"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To execute this logic seamlessly and rapidly inside an agent hook outside the LLM invocation turn, we implemented a Python script using standard library components, alongside a pure Bash fallback script (&lt;code&gt;check_quota.sh&lt;/code&gt;) and an entrypoint runner (&lt;code&gt;entrypoint.sh&lt;/code&gt;) that automatically selects Python when available or Bash on systems without Python installed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Note on Scope&lt;/strong&gt;: The &lt;code&gt;GetUserStatus&lt;/code&gt; endpoint returns the &lt;strong&gt;Five Hour Limit Remaining&lt;/strong&gt; fraction (&lt;code&gt;remainingFraction&lt;/code&gt;) and ISO reset timestamp (&lt;code&gt;resetTime&lt;/code&gt;) for active model pools. The long-term &lt;strong&gt;Weekly Limit Remaining&lt;/strong&gt; is not exposed through this RPC endpoint.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Complete Agent Hook Workflow
&lt;/h2&gt;

&lt;p&gt;Building upon the lifecycle concepts detailed in &lt;a href="https://medium.com/google-cloud/a-developers-guide-to-agent-hooks-in-antigravity-cli-4c1440febd11" rel="noopener noreferrer"&gt;A Developer’s Guide to Agent Hooks in Antigravity CLI&lt;/a&gt;, the plugin integrates into the Antigravity CLI by registering &lt;code&gt;PreInvocation&lt;/code&gt; and &lt;code&gt;PostInvocation&lt;/code&gt; agent hooks in &lt;code&gt;hooks.json&lt;/code&gt;. Because &lt;code&gt;PreInvocation&lt;/code&gt; fires after the user submits input but &lt;em&gt;before&lt;/em&gt; the prompt payload is dispatched to the LLM backend, it inspects local process state and dynamically injects steps prior to model inference.&lt;/p&gt;

&lt;p&gt;As detailed in Figure 2, the final agent hook operates under two distinct execution patterns based on the configured warning threshold (default: 20%):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikh914c63vrs98ufm9cr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikh914c63vrs98ufm9cr.jpg" alt="Figure 2: Complete agent hook execution workflow diagram detailing Pattern A (silent) and Pattern B (warning state)." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: Normal Operation (Quota &amp;gt; Threshold)
&lt;/h3&gt;

&lt;p&gt;When remaining quota is above the warning threshold, the hook outputs an empty step injection payload:&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;"injectSteps"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: &lt;strong&gt;Zero Quota Consumption (0 Token Overhead)&lt;/strong&gt;. The hook executes silently in less than 50 milliseconds. No messages or extra context are injected into the LLM session, consuming absolutely zero model quota.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pattern B: Warning State (Quota &amp;lt;= Threshold)
&lt;/h3&gt;

&lt;p&gt;When remaining quota drops to or below the threshold, the hook injects a transient system message with mandatory agent directives:&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;"injectSteps"&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="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ephemeralMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"⚠️ [SYSTEM QUOTA WARNING] Model quota is below threshold (20%) (Active: gemini-3.6-flash-medium):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - GEMINI Models [ACTIVE MODEL]: 20.0% remaining (Refreshes in 3h 00m)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;[MANDATORY INSTRUCTION FOR AGENT]: The model quota has dropped below the threshold. You MUST display a prominent Quota Warning banner at the very top of your response for THIS TURN ONLY! Do NOT display a warning banner on subsequent turns unless another quota warning is explicitly injected. In the warning banner, you MUST also inform the user that they can run the '/usage' command at any time to inspect detailed quota status."&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;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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: The AI agent immediately prepends a prominent Quota Warning banner to its response, advising the developer to run &lt;code&gt;/usage&lt;/code&gt; or pause heavy multi-step automation before encountering a hard crash.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Installation &amp;amp; Dual Runtime
&lt;/h2&gt;

&lt;p&gt;The complete implementation is published as an open-source Antigravity CLI plugin: &lt;code&gt;antigravity-cli-check-usage-plugin&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Install the plugin directly via the Antigravity CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agy plugin &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/tanaikech/antigravity-cli-check-usage-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dual Runtime Architecture: Python Primary + Pure Bash Fallback
&lt;/h3&gt;

&lt;p&gt;The plugin features a multi-environment entrypoint (&lt;code&gt;entrypoint.sh&lt;/code&gt;) producing 100% identical JSON outputs across both runtimes. The engineering rationale behind this dual design includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python (Primary Runner)&lt;/strong&gt;: Requires zero external dependencies like &lt;code&gt;jq&lt;/code&gt;, absorbs OS-specific syntax differences across Linux, macOS, and Windows, and guarantees type-safe date math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure Bash (Fallback Safety Net)&lt;/strong&gt;: Ensures instant execution in minimal or containerized environments where Python is not pre-installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration and Disabling
&lt;/h3&gt;

&lt;p&gt;You can customize or completely disable the warning threshold (default: &lt;strong&gt;20.0%&lt;/strong&gt;) using environment variables, configuration files, or hook arguments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set Custom Threshold (e.g., 25%):&lt;/strong&gt;&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;QUOTA_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Disable Quota Check Completely:&lt;/strong&gt;&lt;br&gt;
Setting &lt;code&gt;QUOTA_THRESHOLD&lt;/code&gt; to &lt;code&gt;-1&lt;/code&gt; instructs the hook to skip all RPC queries immediately:&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;QUOTA_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Real-World Testing &amp;amp; Verification
&lt;/h2&gt;

&lt;p&gt;After installing the plugin, setting &lt;code&gt;export QUOTA_THRESHOLD=80.0&lt;/code&gt; and executing a live session test in Antigravity CLI v1.1.12 demonstrates the hook in action, as captured in Figure 3:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywasyvlq3u48ootk7lnt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywasyvlq3u48ootk7lnt.jpg" alt="Figure 3: Live terminal demonstration of real-time Quota Warning banner injection in Antigravity CLI 1.1.12." width="799" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user enters a simple greeting (&lt;code&gt;hello&lt;/code&gt;), the agent hook instantly detects that the active model's remaining quota (71.0%) has dropped below the configured threshold (80.0%). A prominent yellow &lt;strong&gt;Warning banner&lt;/strong&gt; (&lt;code&gt;Quota Warning: GEMINI Models quota is at 71.0% remaining...&lt;/code&gt;) is dynamically prepended at the top of the AI's response, alerting the developer and providing a reminder to inspect detailed limits via &lt;code&gt;/usage&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Updating &amp;amp; Uninstalling
&lt;/h2&gt;

&lt;p&gt;To update the plugin to the latest version or remove it from your environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check installed plugins&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  agy plugin list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uninstall the plugin&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  agy plugin uninstall antigravity-cli-check-usage-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reinstall the updated version&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  agy plugin &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/tanaikech/antigravity-cli-check-usage-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we presented a zero-overhead solution to eliminate mid-session quota crashes and account-switching signature errors in Google Antigravity CLI. Drawing upon foundational concepts from &lt;a href="https://medium.com/google-cloud/a-developers-guide-to-agent-hooks-in-antigravity-cli-4c1440febd11" rel="noopener noreferrer"&gt;A Developer’s Guide to Agent Hooks in Antigravity CLI&lt;/a&gt; and resolving the paradox where using LLM tool calls to query internal RPC endpoints consumes quota, we built native CLI Agent Hooks (&lt;code&gt;PreInvocation&lt;/code&gt; / &lt;code&gt;PostInvocation&lt;/code&gt;) running completely outside the LLM execution turn. Featuring a dual Python primary and pure Bash fallback architecture, the hook probes internal local Connect RPC endpoints with absolute zero token consumption during normal operation. By proactively injecting warning banners and &lt;code&gt;/usage&lt;/code&gt; reminders when quota drops below threshold, it guarantees universal environment compatibility and eliminates task interruptions cleanly at the root.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gemini</category>
      <category>antigravity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Latency vs. Tokens: What I Learned Optimizing an Agent with Gemma (and What Didn't Work)</title>
      <dc:creator>leslysandra</dc:creator>
      <pubDate>Wed, 12 Aug 2026 03:32:11 +0000</pubDate>
      <link>https://dev.to/gde/latency-vs-tokens-what-i-learned-optimizing-an-agent-with-gemma-and-what-didnt-work-445g</link>
      <guid>https://dev.to/gde/latency-vs-tokens-what-i-learned-optimizing-an-agent-with-gemma-and-what-didnt-work-445g</guid>
      <description>&lt;p&gt;I'd been waiting for more than 30 minutes. The terminal just sat there, blinking, without returning a single word. I'd launched Gemma2 in its 9-billion-parameter version on my laptop (a regular Mac, the kind any professor or student would use) and the model simply wasn't responding.&lt;/p&gt;

&lt;p&gt;It wasn't a bug. It was the most honest answer the experiment could have given me.&lt;/p&gt;

&lt;p&gt;That frustrating wait ended up being, without exaggeration, the most interesting finding of the whole process. Because the question that brought me there wasn't "how big can a model get?" — it was a much more practical one: &lt;strong&gt;what actually happens when an agent you built in a tutorial has to survive in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been working with Gemma as a case study to understand that jump — from an educational prototype to something that can hold up under long conversations, limited hardware, and real users. This post is the honest summary of that process: what worked convincingly, what didn't work the way I expected, and why that "didn't work" turned out to be more useful than a clean result would have been.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem: why tutorials are a little dishonest
&lt;/h2&gt;

&lt;p&gt;Almost every conversational agent tutorial does the same thing, without saying so out loud: on every turn, it sends the model the &lt;em&gt;entire&lt;/em&gt; previous history, all over again.&lt;/p&gt;

&lt;p&gt;Imagine that every time you added a sentence to a conversation, you had to repeat everything said before it — every message, every reply — before you could say the new one. At first you don't notice. But if the conversation runs 30 or 50 turns, you're repeating an entire novel just to add one sentence.&lt;/p&gt;

&lt;p&gt;This pattern is called &lt;strong&gt;linear context stacking&lt;/strong&gt;, and it causes three concrete problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory saturation&lt;/strong&gt; — every call to the model processes an increasingly large context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk of hitting the token limit&lt;/strong&gt; — every model has a maximum context window; sooner or later, you hit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality degradation&lt;/strong&gt; — there's a documented phenomenon in NLP literature called &lt;em&gt;"lost in the middle"&lt;/em&gt;: when context gets very long, models pay less attention to information sitting in the middle of it, versus the beginning or end. In other words, it's not just slower — it gets worse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This problem isn't unique to any one model, but it weighs differently depending on context. If you're using a closed API with a massive context window and pay-per-token billing, the cost of this problem is financial — you just pay more. But if you're running an open model locally, as is common in universities and research labs across Latin America, the cost is infrastructure: limited RAM, no dedicated GPU, no room to "just pay for more compute." An unbounded context isn't a minor optimization detail there — it's the difference between the agent working at all or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The experiment: design and decisions
&lt;/h2&gt;

&lt;p&gt;To avoid staying purely theoretical, I ran a simple but controlled comparative experiment using &lt;strong&gt;Gemma 2 (2B)&lt;/strong&gt;, running locally with &lt;strong&gt;Ollama&lt;/strong&gt; — no dependency on any paid external API.&lt;/p&gt;

&lt;p&gt;The idea: simulate a typical technical conversation (a microservice troubleshooting case, where each turn adds new information) and run it against two different architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline A (Naive):&lt;/strong&gt; accumulates the entire history with no compression at all. This is, literally, what a tutorial-style agent looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline B (Optimized):&lt;/strong&gt; applies history pruning — instead of sending the whole conversation, it sends a compact summary of the latest state.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pipeline A — accumulates everything, no pruning
&lt;/span&gt;&lt;span class="n"&gt;conversation_history&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Previous text &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;full_prompt&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;conversation_history&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TASK_PROMPT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Pipeline B — only a compact summary of the latest state
&lt;/span&gt;&lt;span class="n"&gt;full_prompt&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;Previous compact context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compact_context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TASK_PROMPT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three methodological decisions I almost overlooked, and which turned out to be key to making the results trustworthy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The "cold start" nearly ruined everything.&lt;/strong&gt;&lt;br&gt;
In my first run, the first step of each pipeline came out suspiciously slower than the ones after it — several seconds off. It wasn't the prompt size: it was the cost of loading the model into memory the first time it's called. The fix was adding a throwaway "warm-up" call before starting to measure each pipeline, so both started on equal footing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Real tokens, not estimated ones.&lt;/strong&gt;&lt;br&gt;
At first I was estimating tokens by counting words and applying an approximate conversion factor — a completely avoidable loss of precision. Ollama returns the real, exact count in every response (&lt;code&gt;prompt_eval_count&lt;/code&gt;). Switching to that number made the charts far more defensible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A single run isn't enough.&lt;/strong&gt;&lt;br&gt;
I ran each pipeline 3 times and averaged the results, with error bars included in the charts. This is what honestly revealed that one of my early results wasn't as solid as it first looked — more on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results: what held up cleanly, and what didn't
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tokens: the result that actually holds
&lt;/h3&gt;

&lt;p&gt;The token pattern was consistent across all 3 runs, with no ambiguity. The naive pipeline grows linearly — from 107 to 266 tokens in just 4 steps, nearly tripling. The optimized pipeline flattens into a plateau, around 104 tokens.&lt;/p&gt;

&lt;p&gt;That's a &lt;strong&gt;61% reduction&lt;/strong&gt; in input tokens by the final step. Active context management delivers exactly what it promises: it keeps the conversation's memory footprint from growing unchecked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr5isr5zwfyddek8jod9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr5isr5zwfyddek8jod9.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency: the result that forced me to rethink the hypothesis
&lt;/h3&gt;

&lt;p&gt;This is where the experiment got genuinely interesting. The intuition says: fewer input tokens, faster response. The real data &lt;strong&gt;didn't back that up&lt;/strong&gt; — at least not clearly. The error bars for the naive and optimized pipelines overlap in almost every step.&lt;/p&gt;

&lt;p&gt;Why? Because with a 2B model, on relatively short conversations, total response time is dominated by how much the model has to &lt;strong&gt;generate&lt;/strong&gt; as output — not by how much it has to &lt;strong&gt;read&lt;/strong&gt; as input. Shrinking the context doesn't automatically speed up the generation of the response.&lt;/p&gt;

&lt;p&gt;It's a "negative" result in the sense that it doesn't confirm the initial hypothesis, but it's honestly the most valuable finding of the whole experiment: context management and latency are related problems, but they're not the same problem, and optimizing one doesn't guarantee improving the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  The failed attempt with Gemma2 9B (and why I'm not hiding it)
&lt;/h2&gt;

&lt;p&gt;I wanted to push one step further and repeat the comparison with Gemma2's 9B version, to see whether a larger model would show a clearer latency advantage — the hypothesis being that processing a long prompt weighs more when the model itself is bigger.&lt;/p&gt;

&lt;p&gt;I never got that data. Over 30 minutes running on my laptop, without a single complete response. I had to cancel it.&lt;/p&gt;

&lt;p&gt;I could have left this out of the post. But it's a relevant data point in its own right, and honestly the one closest to my reality as a researcher in the region of Latin America: &lt;strong&gt;the barrier to experimenting with larger models isn't just a software optimization problem, it's a hardware access problem.&lt;/strong&gt; If I, with intent and dedicated time, struggle to run a 9B model on a consumer laptop, that's exactly why this kind of work — optimizing efficient agents with small, accessible models — matters for universities, labs, and teams in the region that don't have dedicated GPUs on hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;If you're building, or thinking about building, an agent on a local open model, here's what I'm taking away from this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you optimize.&lt;/strong&gt; My initial intuition about latency was not the correct one, and I only found out because I measured rigorously (3 runs, warm-up, real tokens) instead of trusting a single run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saving tokens doesn't automatically buy you latency.&lt;/strong&gt; Depending on model size and conversation length, the real bottleneck might be somewhere else entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context pruning has trade-offs — it's not magic.&lt;/strong&gt; My current implementation trims by length, not semantic relevance, which means there's real risk of losing important historical information. That's a limitation I'm naming, not hiding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed experiment on real hardware is data, not a failure.&lt;/strong&gt; I couldn't run 9B on my laptop. That data point ends up being as useful to the argument of this work as any chart.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;This experiment started from a simple question — how do you take a tutorial-style agent and make it survive production? — and ended up giving me a more nuanced answer than I expected: context management matters, a lot, but it doesn't solve every performance problem on its own, and hardware constraints are a legitimate part of the technical conversation, not just a logistics footnote.&lt;/p&gt;

&lt;p&gt;All the code is available in the &lt;a href="https://github.com/leslysandra/gemma-agent-nlp-optimization" rel="noopener noreferrer"&gt;repository&lt;/a&gt; for anyone who wants to reproduce or adapt it — including both the successful results with Gemma2 (2B) and the documented limitation with the 9B model, because I believe transparency about what didn't work is as valuable as what did.&lt;/p&gt;

&lt;p&gt;If you're working with open models in the region, I'd genuinely love to hear about your experience — what hardware you're running, what you've hit, what context management strategies have worked for you. Reach out on &lt;a href="https://www.linkedin.com/in/lesly-zerna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This work was also presented as a poster at the &lt;a href="https://south-american-nlp-school.dc.uba.ar/" rel="noopener noreferrer"&gt;Second South American NLP School&lt;/a&gt; (Buenos Aires, August 2026).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsevvhqb9x9mpn4jel4vc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsevvhqb9x9mpn4jel4vc.png" alt=" " width="667" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3igvh65hehu066skknk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3igvh65hehu066skknk.png" alt=" " width="709" height="763"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gemma</category>
      <category>genai</category>
      <category>experiment</category>
      <category>education</category>
    </item>
    <item>
      <title>The unofficial TPU migration guide: Cloud TPU API to Compute Engine</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 11 Aug 2026 19:46:01 +0000</pubDate>
      <link>https://dev.to/gde/the-unofficial-tpu-migration-guide-cloud-tpu-api-to-compute-engine-2co7</link>
      <guid>https://dev.to/gde/the-unofficial-tpu-migration-guide-cloud-tpu-api-to-compute-engine-2co7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8a0u6ehw33amgxt7wbw5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8a0u6ehw33amgxt7wbw5.jpg" alt="TPU Migration" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.cloud.google.com/tpu/docs/tpus-in-compute-engine" rel="noopener noreferrer"&gt;Cloud TPU resources in Compute Engine&lt;/a&gt; puts it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Cloud TPU API is no longer under active development. This includes the Google Cloud CLI for the Cloud TPU API and the Cloud Client Libraries for the Cloud TPU API. The Cloud TPU API will receive bug fixes and security updates only.&lt;/p&gt;

&lt;p&gt;New hardware generations, starting with TPU7x (Ironwood), are supported only through Compute Engine or Google Kubernetes Engine (GKE).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No sunset date is published, so nothing breaks on a deadline. But the second sentence is the forcing function: the API you are on today is the API your next chip will not support.&lt;/p&gt;

&lt;p&gt;So I moved a rig over — a v6e-1 (Trillium) chip serving &lt;code&gt;gemma-4-E2B-it&lt;/code&gt; under vLLM, rebuilt on &lt;code&gt;gcloud compute instances&lt;/code&gt;. Same chip, same checkpoint, same serving flags, only the control plane changed.&lt;/p&gt;

&lt;p&gt;The flag mapping was the quick part. Everything after it — the quota model, a dead boot, tooling that had silently gone blind — took far longer, because &lt;strong&gt;almost nothing on this path fails loudly.&lt;/strong&gt; What follows is what changes, what bit me, and how to tell one failure from another.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes
&lt;/h2&gt;

&lt;p&gt;The short version, so the rest makes sense.&lt;/p&gt;

&lt;p&gt;Old:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud alpha compute tpus queued-resources create vllm-gemma4-qr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vllm-gemma4-qr-node &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east5-b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--accelerator-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v6e-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v2-alpha-tpuv6e &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;flex-start &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--valid-until-duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instances create gce-vllm-v6e1-2b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west4-a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ct6e-standard-1t &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu-accel-2204-amd64-tpu-v5e-v5p-v6e &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu-os-accelerator-images &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--maintenance-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERMINATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--boot-disk-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200GB &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cloud-platform &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata-from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;startup-script&lt;span class="o"&gt;=&lt;/span&gt;/tmp/startup.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FLEX_START &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request-valid-for-duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-run-duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-termination-action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DELETE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud TPU API&lt;/th&gt;
&lt;th&gt;Compute Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--accelerator-type=v6e-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--machine-type=ct6e-standard-1t&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--runtime-version=v2-alpha-tpuv6e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--image-family=ubuntu-accel-...&lt;/code&gt; + &lt;code&gt;--image-project&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--valid-until-duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--request-valid-for-duration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--provisioning-model=flex-start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--provisioning-model=FLEX_START&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QR produces a node named &lt;code&gt;&amp;lt;id&amp;gt;-node&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the instance &lt;strong&gt;is&lt;/strong&gt; the node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gcloud compute tpus tpu-vm list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcloud compute instances list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gcloud compute tpus tpu-vm ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcloud compute ssh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One documentation trap before you start, and it is a trap in both directions. &lt;a href="https://docs.cloud.google.com/tpu/docs/request-using-flex-start" rel="noopener noreferrer"&gt;Request TPU Flex-start VMs&lt;/a&gt; states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You must use the queued resources API to use TPU Flex-start VMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;That is true for v5e and out of date for everything else.&lt;/strong&gt; The page sits in the deprecated API's doc set, describes flex-start within that API, and never mentions &lt;code&gt;instances create&lt;/code&gt;. For v5e it is still correct — there is no Compute Engine path at all, as above. For v5p, v6e and TPU7x it will send you to the API you are trying to leave.&lt;/p&gt;

&lt;p&gt;The Compute Engine &lt;a href="https://docs.cloud.google.com/compute/docs/instances/provisioning-models" rel="noopener noreferrer"&gt;provisioning models&lt;/a&gt; page is the one to believe for those three. It lists the flex-start machine series as "A4, A3, A2, G4, and G2" plus "TPU7x, TPU v6e, and TPU v5p"; &lt;code&gt;instances create&lt;/code&gt; takes &lt;code&gt;FLEX_START&lt;/code&gt; as a first-class value; and &lt;code&gt;--request-valid-for-duration&lt;/code&gt; is its wait knob, capped at two hours for a standalone VM. Every flex-start instance in this article was created that way.&lt;/p&gt;

&lt;p&gt;One caveat if you are planning ahead: flex-start on &lt;strong&gt;TPU7x is behind an allowlist&lt;/strong&gt;, per a footnote on that page — contact your account team. v5p and v6e are ungated.&lt;/p&gt;

&lt;p&gt;Two genuine wins while we are here. &lt;strong&gt;&lt;code&gt;--max-run-duration&lt;/code&gt; is not flex-start's alone.&lt;/strong&gt; On the TPU API, gcloud documents the flag as "Used with flex-start"; on Compute Engine I have used it on spot creates as well, so at least those two models can carry it. Pair it with &lt;code&gt;--instance-termination-action=DELETE&lt;/code&gt; and a demo box cleans up after itself. And &lt;strong&gt;the two-object lifecycle collapses&lt;/strong&gt;: no queued resource owning a node you did not name, no reconciling the two names, and teardown needs no &lt;code&gt;--force&lt;/code&gt; — on the old path deleting an ACTIVE resource did.&lt;/p&gt;

&lt;p&gt;Serving does not change. Same chip, same engine build, same flags: the KV cache allocation came out at &lt;strong&gt;1,151,744 tokens on both control planes&lt;/strong&gt; — the same integer, not merely close — and throughput matched to 0.6% on the control cells. Larger cells varied by a few percent in both directions, but that benchmark swings further than that on cache state alone, so I would not read anything into it. &lt;strong&gt;This is not a performance decision.&lt;/strong&gt; Plan it as a refactor.&lt;/p&gt;

&lt;p&gt;Now the parts that cost real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every chip has a Compute Engine path
&lt;/h2&gt;

&lt;p&gt;Check before you plan anything, and do not check by looking in the machine-type catalog, because it will tell you yes when the answer is no.&lt;/p&gt;

&lt;p&gt;v5e looks fine there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud compute machine-types list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name~ct5lp"&lt;/span&gt;
&lt;span class="go"&gt;NAME              ZONE           CPUS  MEMORY_GB  GUEST_ACCELERATOR_TYPE
ct5lp-hightpu-1t  us-central1-a  24    48.00      ['ct5lp']
ct5lp-hightpu-4t  us-central1-a  112   192.00     ['ct5lp']
ct5lp-hightpu-8t  us-central1-a  224   384.00     ['ct5lp']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three shapes, 26 zones. Now create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: (gcloud.compute.instances.create) Could not fetch resource:
 - This user agent is not allowed to use the machine type [ct5lp-hightpu-1t].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refused outright. Not a quota error, not a does-not-exist error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GKE uses those exact machine type names&lt;/strong&gt; — its TPU documentation describes &lt;code&gt;ct5lp-hightpu-4t&lt;/code&gt; and its topologies directly. That gives the catalog entry a consumer who is not you, which is my best explanation for why the strings exist without a create path, though Google does not say so outright. The same reasoning covers the other things that look like a v5e path and are not: the image family is literally named &lt;code&gt;ubuntu-accel-2204-amd64-tpu-v5e-v5p-v6e&lt;/code&gt;, and there is a &lt;code&gt;compute.googleapis.com&lt;/code&gt; quota metric called &lt;code&gt;TPU-LITE-PODSLICE-V5-per-project-zone&lt;/code&gt;. Whatever the reason, Compute-Engine-shaped artifacts exist for v5e without a Compute Engine create path, so do not treat any of them as evidence of one.&lt;/p&gt;

&lt;p&gt;The public docs agree, if you read them closely. &lt;a href="https://docs.cloud.google.com/compute/docs/tpus/tpu-machines" rel="noopener noreferrer"&gt;TPU machines in the accelerator-optimized family&lt;/a&gt; says "Compute Engine supports the following TPU versions: TPU7x, TPU v6e, TPU v5p" and does not mention v5e anywhere. And the &lt;a href="https://docs.cloud.google.com/tpu/docs/v5e" rel="noopener noreferrer"&gt;TPU v5e&lt;/a&gt; page says v5e "is supported using Google Kubernetes Engine and the Cloud TPU API", with Compute Engine absent from that list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catalog presence is not creatability.&lt;/strong&gt; Testing costs nothing: pick a zone where your quota is zero and try the create. A rejection is free and conclusive.&lt;/p&gt;

&lt;h2&gt;
  
  
  A primer on the four provisioning models
&lt;/h2&gt;

&lt;p&gt;Compute Engine gives you four ways to ask for a chip, and the choice drives everything downstream — what you pay, which quota you spend, and how you fail. Worth ten minutes up front.&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;how you get capacity&lt;/th&gt;
&lt;th&gt;max run&lt;/th&gt;
&lt;th&gt;how it ends&lt;/th&gt;
&lt;th&gt;quota spent&lt;/th&gt;
&lt;th&gt;v6e, europe-west4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;STANDARD&lt;/code&gt;&lt;/strong&gt; (on-demand)&lt;/td&gt;
&lt;td&gt;immediately, if available&lt;/td&gt;
&lt;td&gt;unlimited&lt;/td&gt;
&lt;td&gt;when you say so&lt;/td&gt;
&lt;td&gt;standard&lt;/td&gt;
&lt;td&gt;$2.97/chip-hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;SPOT&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;immediately, if available&lt;/td&gt;
&lt;td&gt;unlimited&lt;/td&gt;
&lt;td&gt;preempted whenever Google wants it back&lt;/td&gt;
&lt;td&gt;preemptible → standard&lt;/td&gt;
&lt;td&gt;$1.78/chip-hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;FLEX_START&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;queues, up to a 2h wait&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 min – 7 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;at &lt;code&gt;--max-run-duration&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;preemptible → standard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.35/chip-hr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;RESERVATION_BOUND&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;reserved ahead, if approved&lt;/td&gt;
&lt;td&gt;up to 90 days (calendar)&lt;/td&gt;
&lt;td&gt;when the reservation ends&lt;/td&gt;
&lt;td&gt;managed with the reservation&lt;/td&gt;
&lt;td&gt;no list rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Behaviour from the &lt;a href="https://docs.cloud.google.com/compute/docs/instances/provisioning-models" rel="noopener noreferrer"&gt;provisioning models&lt;/a&gt; page — the reservation-bound quota cell is a simplification, since that page says it varies by reservation type. Prices read live from the Cloud Billing Catalog on 2026-08-11.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex-start is the default worth reaching for, and it is the cheapest.&lt;/strong&gt; That surprised me — it undercuts spot on v6e in both regions I priced ($1.35 against $1.78 in europe-west4, $1.40 in us-east5), and it is less than half on-demand. You trade immediacy for it: the request queues rather than failing, for up to two hours, and the instance self-terminates at a duration you set. For serving experiments and benchmarks that is the right shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spot is not the cheap option here, despite the name.&lt;/strong&gt; On v6e it costs &lt;em&gt;more&lt;/em&gt; than flex-start, and it can be reclaimed at any time — with less warning than you might assume, since the preemption notice duration defaults to zero and the shutdown period is best-effort up to 30 seconds. I checked v5e as well, expecting the ordering to invert, and it does not: in us-west4 flex-start is $0.60 against spot's $0.607. Spot's one real advantage is that it does not queue — which makes it useful as a diagnostic, see below. Read the rate rather than assuming either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-demand is for when you cannot tolerate a queue or a deadline.&lt;/strong&gt; Twice the price, no run limit, and it is the only model that spends the family quota directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reservation-bound is the one to know exists rather than the one to start with.&lt;/strong&gt; You ask for capacity at a future date; if Google approves, you get a reservation and your instances bind to it. Calendar mode runs up to 90 days. It has &lt;strong&gt;no list rate in the billing catalog&lt;/strong&gt; — what it costs is whatever the reservation was priced at — so if you have a cost tool, teach it to say "read the reservation" rather than falling back to the on-demand SKU. The old path had a counterpart, for what it is worth: &lt;code&gt;queued-resources create&lt;/code&gt; takes a &lt;code&gt;--reserved&lt;/code&gt; flag to schedule against reserved capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical default:&lt;/strong&gt; flex-start for anything time-boxed, on-demand when you need it now and unbounded, reservation-bound when you have a date and a budget, spot rarely on v6e.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota is the gate, and it is not the quota you expect
&lt;/h2&gt;

&lt;p&gt;Three separate things went wrong for me here, so take them in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your TPU API quota does not come with you
&lt;/h3&gt;

&lt;p&gt;The two control planes meter against completely disjoint pools. My project holds &lt;strong&gt;512 v6e chips in us-east5 on the TPU API&lt;/strong&gt; and, on Compute Engine, held &lt;strong&gt;nothing at all&lt;/strong&gt; in the same region for the same silicon. That is why my first create failed in the zone my rig had used happily for months.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Which quota you spend depends on the provisioning model
&lt;/h3&gt;

&lt;p&gt;There are two v6e quotas on Compute Engine, and picking the wrong one to check is the easiest mistake to make:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provisioning model&lt;/th&gt;
&lt;th&gt;Quota id it spends&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FLEX_START&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PREEMPTIBLE-TPU-V6E-per-project-region&lt;/code&gt;, falling back to the family quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SPOT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PREEMPTIBLE-TPU-V6E-per-project-region&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;STANDARD&lt;/code&gt; (on-demand)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TPUS-PER-TPU-FAMILY-per-project-region&lt;/code&gt;, &lt;code&gt;tpu_family=CT6E&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flex-start spends the preemptible pool.&lt;/strong&gt; That is counterintuitive — flex-start is not preemptible in behaviour, once granted it runs uninterrupted for up to seven days — and nothing in the flag names hints at it. The &lt;a href="https://docs.cloud.google.com/compute/docs/instances/provisioning-models" rel="noopener noreferrer"&gt;provisioning models&lt;/a&gt; page says so, and the second sentence matters as much as the first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you create a Flex-start VM, preemptible quota is consumed. If your project lacks preemptible quota, then standard quota is consumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So for flex-start, &lt;strong&gt;a region is usable if either pool has room.&lt;/strong&gt; Check the preemptible metric first, because that is what gets spent, but do not write a region off on one listing alone.&lt;/p&gt;

&lt;p&gt;I spent a day trying to establish this experimentally before finding it documented — and then, having found it, still got it wrong by quoting only the first sentence. Read the whole entry.&lt;/p&gt;

&lt;p&gt;Note there is &lt;strong&gt;no non-preemptible v6e id at all&lt;/strong&gt; — no &lt;code&gt;TPU-V6E-per-project-region&lt;/code&gt; exists — which is why on-demand falls back to the generic family quota. v4, v5e and v5p each publish their own dedicated pair, so this fallback is a v6e and TPU7x quirk rather than a rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The obvious command does not show either of them
&lt;/h3&gt;

&lt;p&gt;This is the part that cost me the most, because it answers confidently and wrongly:&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;$ &lt;/span&gt;gcloud compute regions describe us-east5 &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(quotas.list())"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;TPU
TPU_LITE_DEVICE_V5               0.0
PREEMPTIBLE_TPU_LITE_DEVICE_V5   0.0
TPU_LITE_PODSLICE_V5             32.0
PREEMPTIBLE_TPU_LITE_PODSLICE_V5 1536.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four metrics, all v5e, &lt;strong&gt;none of which governs v6e.&lt;/strong&gt; The regional quota view only carries the older metrics. v6e lives in the newer Cloud Quotas API and has to be asked for by name — once per metric:&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;$ &lt;/span&gt;gcloud alpha quotas info describe PREEMPTIBLE-TPU-V6E-per-project-region &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;compute.googleapis.com          &lt;span class="c"&gt;# flex-start and spot&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud alpha quotas info describe TPUS-PER-TPU-FAMILY-per-project-region &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;compute.googleapis.com          &lt;span class="c"&gt;# on-demand&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Read both, because their defaults are opposite.&lt;/strong&gt; A region absent from the family listing inherits &lt;strong&gt;0&lt;/strong&gt;. A region absent from the preemptible listing inherits &lt;strong&gt;1536&lt;/strong&gt;. So a region that looks dead in one listing may have plenty of headroom in the other — which is exactly the mistake I made, writing off regions as unusable when only their on-demand path was.&lt;/p&gt;

&lt;p&gt;An unset value also reads identically to a zero one, so a blank does not tell you the hardware is missing. Check &lt;code&gt;machine-types list&lt;/code&gt; for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  What my project actually holds
&lt;/h3&gt;

&lt;p&gt;After the requests below, for the twelve regions that publish &lt;code&gt;ct6e-standard-1t&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;region&lt;/th&gt;
&lt;th&gt;flex-start / spot&lt;/th&gt;
&lt;th&gt;on-demand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;europe-west4, asia-east1, asia-northeast1, asia-south1, asia-southeast1, southamerica-east1, southamerica-west1, us-south1&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-east1&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-central1, us-west1&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-east5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-east4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth reading off that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;us-central1&lt;/code&gt; and &lt;code&gt;us-west1&lt;/code&gt; look unusable if you only check on-demand, and are in fact fine for flex-start — they hold the full 1536 on the pool flex-start actually spends. That is the opposite-defaults trap doing real damage: I wrote both off for a day.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;us-east5&lt;/code&gt; sits at &lt;strong&gt;32&lt;/strong&gt; where every other live region has 1536. I put it there by asking for 32, not realising the preemptible metric defaults to 1536. When I noticed and went back to ask for 1536, &lt;strong&gt;that request was denied&lt;/strong&gt; — so the 32 was not the self-inflicted ceiling it looked like. us-east5 simply is not giving out more today, whatever number you put in the form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting quota and capacity
&lt;/h2&gt;

&lt;p&gt;These two produce the same symptoms and have different fixes, so this is the part worth having a routine for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The symptom: your create sits in PENDING
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PENDING&lt;/code&gt; means either &lt;strong&gt;no quota&lt;/strong&gt; or &lt;strong&gt;no capacity&lt;/strong&gt;, and from the outside they are identical. I produced both separately: a flex-start create in a region with zero quota queued indefinitely, and a flex-start create in a region with 1536 chips of quota and no hardware did exactly the same. In neither case did the create report the actual problem.&lt;/p&gt;

&lt;p&gt;It is not even consistent. In a third zone the same create came back immediately with an explicit &lt;code&gt;reason: stockout&lt;/code&gt; rather than queueing. &lt;strong&gt;So you cannot infer the cause from the behaviour, and "did my create succeed" is not a quota test.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The routine
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — probe capacity with a spot create.&lt;/strong&gt; Spot does not queue; it fails fast and names the reason, which makes it a free capacity check that takes seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute instances create probe --zone=us-central1-a \
    --machine-type=ct6e-standard-1t --provisioning-model=SPOT ...

reason: stockout
zonesAvailable: ''
message: The zone '.../zones/us-central1-a' does not have enough resources
  available to fulfill the request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stockout means your flex-start request is queued behind real scarcity and no amount of quota will help. If spot provisions instead, capacity exists — delete it and go look at quota. (Spot and flex-start draw on the same preemptible pool, so this probes the zone rather than your entitlement.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — try the sibling zones, not just the region.&lt;/strong&gt; Quota is regional; capacity is zonal, and they diverge sharply. In &lt;code&gt;us-central1&lt;/code&gt; I got a stockout in &lt;code&gt;-a&lt;/code&gt;, a stockout in &lt;code&gt;-c&lt;/code&gt;, and a working instance in &lt;code&gt;-b&lt;/code&gt;, all within a few minutes and all against the same 1536-chip regional quota. &lt;strong&gt;If one zone is dry, the next one in the same region costs nothing to try.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — check both quota metrics, not one.&lt;/strong&gt; Covered above: flex-start spends the preemptible pool first and falls back to the family quota, and the two carry opposite defaults. A region that looks dead in one listing may be fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — only then request more quota.&lt;/strong&gt; One command per metric, and the dimension keys differ — the family quota takes &lt;code&gt;region&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;tpu_family&lt;/code&gt;, the preemptible one takes &lt;code&gt;region&lt;/code&gt; alone. Read them off &lt;code&gt;gcloud quotas info describe &amp;lt;quota-id&amp;gt;&lt;/code&gt; rather than guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud quotas preferences create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;compute.googleapis.com &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quota-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PREEMPTIBLE-TPU-V6E-per-project-region &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"region=us-east5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preferred-value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;32 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preference-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;preemptible-tpu-v6e-us-east5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--justification&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check anything you file with &lt;code&gt;gcloud quotas preferences list&lt;/code&gt;. And know what you are likely to get.&lt;/p&gt;

&lt;h3&gt;
  
  
  What quota requests actually do
&lt;/h3&gt;

&lt;p&gt;I filed requests on both metrics across five regions, then retried the denials — all of them once at the same size, and the four that could be lowered again at 8 chips. Every decision came back &lt;strong&gt;within seconds&lt;/strong&gt;, automated, with &lt;code&gt;quotaConfig.stateDetail&lt;/code&gt; carrying the verdict:&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;td&gt;us-east5 preemptible → 32, us-east5 family → 32, us-east1 family → 32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;us-central1 family, us-west1 family, us-east4 family, us-east4 preemptible, us-east5 preemptible → 1536&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;denied again at 8 chips&lt;/td&gt;
&lt;td&gt;us-central1 family, us-west1 family, us-east4 family, us-east4 preemptible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refused at submission&lt;/td&gt;
&lt;td&gt;us-central1 / us-east1 / us-west1 preemptible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things fall out of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The size of the ask is not the variable.&lt;/strong&gt; The same 0 → 32 request was approved in two regions and denied in three. Retried at 8 chips, the denials were identical. Three sizes tested — 8, 32, 1536 — and the outcome tracked the &lt;em&gt;region&lt;/em&gt; every time. There is no magic number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Denials may track capacity.&lt;/strong&gt; Of the regions that denied me quota, the two I went on to probe — us-central1 and us-west1 — both refused a spot create for lack of capacity. I did not probe us-east4, so this is a suggestive pattern across two regions rather than a rule, and the API says nothing about its reasoning. It is at least a reason not to read a denial as a judgement about your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot ask for less than you hold.&lt;/strong&gt; Three requests never reached review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED_PRECONDITION: The quota override ... decreases effective quota unsafely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those regions already sat at the 1536 default and I asked for 32. Because the two metrics carry different defaults, one blanket number is wrong about half the time — read the current value per metric first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quota is a ceiling, not an allocation
&lt;/h3&gt;

&lt;p&gt;The thing to internalise: &lt;strong&gt;holding quota does not mean the hardware is there.&lt;/strong&gt; Single v6e chips were scarce in most places I looked — europe-west4-a served me first time, and everywhere else was a fight.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;zone&lt;/th&gt;
&lt;th&gt;quota held&lt;/th&gt;
&lt;th&gt;spot create&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;europe-west4-a&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;provisioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-central1-a&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reason: stockout&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-central1-b&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;provisioned, then stocked out a minute later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-central1-c&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reason: stockout&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-west1-c&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reason: stockout&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of five zones had full quota and no chips at all. &lt;code&gt;us-central1-b&lt;/code&gt; is the one to remember: an instance came up there, I deleted it, and a request a minute later was refused for stockout. &lt;strong&gt;Availability moves faster than you can test against it&lt;/strong&gt;, let alone plan around.&lt;/p&gt;

&lt;p&gt;So treat quota as permission to ask, not as reserved hardware. Flex-start's queue is the mechanism that actually gets you a chip, because it waits rather than failing — which is worth more here than any amount of quota on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The image is not the runtime version
&lt;/h2&gt;

&lt;p&gt;The first boot of my migrated rig died 100 seconds in, for a reason no flag mapping would have caught:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+ sudo docker pull vllm/vllm-tpu:nightly
sudo: docker: command not found
...
ERROR: Failed to pull vLLM Docker image after multiple retries. Exiting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;ubuntu-accel-2204-amd64-tpu-v5e-v5p-v6e&lt;/code&gt; has no &lt;code&gt;docker&lt;/code&gt; on PATH at first boot.&lt;/strong&gt; The same script had worked unchanged for months on the TPU API's &lt;code&gt;v2-alpha-tpuv6e&lt;/code&gt; runtime, which is why I assume that image ships Docker — I have not inspected it. Either way, the script came across verbatim and went straight for the pull.&lt;/p&gt;

&lt;p&gt;Install it first:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt;
  &lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-qq&lt;/span&gt; docker.io
  &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And fix it in three places, not one: the startup script, any Docker command your tooling runs over SSH (the recovery tool you grab after a failed boot must not fail the same way), and any copy-pasteable deploy one-liner you emit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The general form:&lt;/strong&gt; your startup script was written against a runtime version that gave you things for free. Mine assumed Docker. Whatever yours assumes, the instance will sit there reporting &lt;code&gt;RUNNING&lt;/code&gt; while it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  RUNNING does not mean ready
&lt;/h2&gt;

&lt;p&gt;This is the most misleading signal on the new path.&lt;/p&gt;

&lt;p&gt;A queued resource reached &lt;code&gt;ACTIVE&lt;/code&gt; only once its node was up. &lt;strong&gt;An instance is &lt;code&gt;RUNNING&lt;/code&gt; the moment the VM boots&lt;/strong&gt; — before the startup script has pulled an image, loaded a model, or done anything at all. During the entire failed boot above, the instance list said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME              ZONE            MACHINE_TYPE      STATUS
gce-vllm-v6e1-2b  europe-west4-a  ct6e-standard-1t  RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It said that indefinitely. Nothing distinguishes a dead boot from a healthy one except reading the startup log or curling the port. Any readiness check you ported that trusted &lt;code&gt;ACTIVE&lt;/code&gt; is now wrong — by several minutes on a good day, and forever on a bad one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery and SSH move, and the old calls go quiet
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;ct6e-*&lt;/code&gt; instance is an ordinary Compute Engine instance that happens to carry a TPU, so the old API cannot see it:&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;$ &lt;/span&gt;gcloud compute instances list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name=gce-vllm-v6e1-2b"&lt;/span&gt;
NAME              ZONE            MACHINE_TYPE      STATUS
gce-vllm-v6e1-2b  europe-west4-a  ct6e-standard-1t  RUNNING

&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud compute tpus tpu-vm list &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west4-a
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty. No error, no warning — your tooling simply believes nothing is running. Two field shapes move with it: status is &lt;code&gt;status: RUNNING&lt;/code&gt; rather than &lt;code&gt;state: READY&lt;/code&gt;, and the external IP moves from &lt;code&gt;networkEndpoints[].accessConfig.externalIp&lt;/code&gt; to &lt;code&gt;networkInterfaces[].accessConfigs[].natIP&lt;/code&gt;. Copy the old status check across and it will not throw; it will just sort every healthy instance to the bottom of your ranking, which you notice the day you have two.&lt;/p&gt;

&lt;p&gt;SSH moves too, and this is the call site people miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute tpus tpu-vm ssh &amp;lt;node&amp;gt;   &lt;span class="c"&gt;# old&lt;/span&gt;
gcloud compute ssh &amp;lt;instance&amp;gt;           &lt;span class="c"&gt;# new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything that manages your container, tails logs, reads journalctl or runs a benchmark has to move — and those are precisely the tools you reach for &lt;em&gt;when something has already gone wrong&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I had a test asserting my rig was off the old API. It covered the discovery function. Four other tools were still calling &lt;code&gt;tpu-vm ssh&lt;/code&gt; behind its back, plus several Makefile targets. &lt;strong&gt;Grep for the old command; do not trust one test over one function.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three flags that fail late
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--scopes=cloud-platform&lt;/code&gt;&lt;/strong&gt; — required if your startup script reads a secret. Mine pulls a Hugging Face token from Secret Manager at boot. Without the scope the VM boots fine and then spins for 30 minutes before giving up, so the symptom is a slow startup followed by what looks like a token problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--boot-disk-size&lt;/code&gt;&lt;/strong&gt; — the image default is 10 GB, which will not hold a vLLM TPU image. Fails after a clean boot, mid-pull, which is a long way from the flag you got wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--maintenance-policy=TERMINATE&lt;/code&gt;&lt;/strong&gt; — required, because a TPU instance cannot live-migrate.&lt;/p&gt;

&lt;p&gt;Two more worth knowing, both from the &lt;a href="https://docs.cloud.google.com/compute/docs/instances/provisioning-models" rel="noopener noreferrer"&gt;provisioning models&lt;/a&gt; page. Flex-start instances run for a minimum of 10 minutes and &lt;strong&gt;a maximum of seven days&lt;/strong&gt;, so set &lt;code&gt;--max-run-duration&lt;/code&gt; explicitly rather than discovering the boundary. And you cannot suspend one — a standalone flex-start instance can be stopped, but suspend and recreate are unavailable, and anything created through a MIG resize request cannot be stopped either. Keep state you care about on a separate disk or in GCS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting quick reference
&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;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PENDING&lt;/code&gt; for hours&lt;/td&gt;
&lt;td&gt;quota &lt;strong&gt;or&lt;/strong&gt; capacity — identical from outside&lt;/td&gt;
&lt;td&gt;fire a SPOT create at the same zone; &lt;code&gt;stockout&lt;/code&gt; means capacity, and usually it is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;This user agent is not allowed to use the machine type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;that generation has no Compute Engine path&lt;/td&gt;
&lt;td&gt;use the Cloud TPU API for that chip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;RUNNING&lt;/code&gt; but nothing serves&lt;/td&gt;
&lt;td&gt;startup script died&lt;/td&gt;
&lt;td&gt;read the startup log; curl the port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the CE image ships no Docker&lt;/td&gt;
&lt;td&gt;install &lt;code&gt;docker.io&lt;/code&gt; before pulling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of disk mid-pull&lt;/td&gt;
&lt;td&gt;10 GB image default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--boot-disk-size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret access hangs 30 min&lt;/td&gt;
&lt;td&gt;missing &lt;code&gt;--scopes=cloud-platform&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;recreate with the scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex-start VM disappeared&lt;/td&gt;
&lt;td&gt;it reached &lt;code&gt;--max-run-duration&lt;/code&gt;, max seven days&lt;/td&gt;
&lt;td&gt;set the duration explicitly; it is not unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tpu-vm list&lt;/code&gt; returns nothing&lt;/td&gt;
&lt;td&gt;wrong API for a &lt;code&gt;ct6e-*&lt;/code&gt; instance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcloud compute instances list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH says not found&lt;/td&gt;
&lt;td&gt;wrong SSH surface&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gcloud compute ssh&lt;/code&gt;, not &lt;code&gt;tpus tpu-vm ssh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota looks fine but nothing works&lt;/td&gt;
&lt;td&gt;reading &lt;code&gt;regions describe&lt;/code&gt;, which shows v5 metrics only&lt;/td&gt;
&lt;td&gt;Cloud Quotas API, by metric name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;decreases effective quota unsafely&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;requesting less than you hold&lt;/td&gt;
&lt;td&gt;read the current value first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gcloud alpha&lt;/code&gt; reported missing but works&lt;/td&gt;
&lt;td&gt;apt install, component manager disabled by design&lt;/td&gt;
&lt;td&gt;ignore it; alpha ships in the base package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Check whether your chip has a Compute Engine path at all, by trying a create rather than reading the catalog. Check your quota through the Cloud Quotas API rather than &lt;code&gt;regions describe&lt;/code&gt;, and read it as an intersection with machine-type availability. Translate the flags. Then rewrite discovery and SSH instead of filtering them, and grep for the old commands afterwards.&lt;/p&gt;

&lt;p&gt;And assume nothing fails loudly. A stuck request, a dead boot, a blind discovery helper and a missing SSH surface all present as silence or as a cheerful &lt;code&gt;RUNNING&lt;/code&gt;. The flag mapping is the part gcloud checks for you; everything in this article is the part it does not.&lt;/p&gt;

&lt;p&gt;There is nothing as constant as change. TPU7x is already Compute Engine only, so this will not be the last migration any of us does — but the next one should be cheaper, because the hard part was never the flags.&lt;/p&gt;

</description>
      <category>tpu</category>
      <category>gcp</category>
      <category>vllm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Gubernator v2.13.0: Google SRE SLOs, Native CoreDNS Suite &amp; Caddy Ingress for Docker Compose</title>
      <dc:creator>Mario Ezquerro</dc:creator>
      <pubDate>Tue, 11 Aug 2026 06:14:53 +0000</pubDate>
      <link>https://dev.to/gde/gubernator-v2130-google-sre-slos-native-coredns-suite-caddy-ingress-for-docker-compose-1bac</link>
      <guid>https://dev.to/gde/gubernator-v2130-google-sre-slos-native-coredns-suite-caddy-ingress-for-docker-compose-1bac</guid>
      <description>&lt;p&gt;If you love the &lt;strong&gt;simplicity of Docker Swarm&lt;/strong&gt; (native Compose files, lightweight single binary) but miss the &lt;strong&gt;advanced capabilities of Kubernetes&lt;/strong&gt; (targeted label placement, SRE-grade observability, built-in DNS service discovery, and zero-trust ingress), meet &lt;strong&gt;Gubernator (gbnt)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We are excited to release &lt;strong&gt;Gubernator v2.13.0&lt;/strong&gt;, introducing three massive feature suites natively integrated into a single binary and a modern Material Design 3 Flutter Web Dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Google SRE Multi-Burn-Rate SLO Engine &amp;amp; Interactive Suite&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CoreDNS 4-Tab Management Suite &amp;amp; Interactive Dig Playground&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caddy Ingress &amp;amp; Zero-Trust Reverse Proxy Suite&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Fun Fact: The entirety of Gubernator's codebase, multi-node deployment pipelines, and SRE features were designed, built, and pair-programmed using **Google Antigravity (AGY)&lt;/em&gt;&lt;em&gt;, Google DeepMind's agentic AI coding assistant!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's dive into what's new and how you can level up your self-hosted or production container clusters!&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Google SRE Multi-Burn-Rate SLO Engine &amp;amp; Web Suite
&lt;/h2&gt;

&lt;p&gt;Defining &lt;strong&gt;Service Level Objectives (SLOs)&lt;/strong&gt; and tracking &lt;strong&gt;Error Budgets&lt;/strong&gt; is the gold standard of Site Reliability Engineering. Until now, implementing SLOs meant running heavy Kubernetes CRDs (via tools like Sloth or Pyrra) or using costly SaaS platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gubernator v2.13.0&lt;/strong&gt; brings Google SRE Workbook (Chapter 5) compliant multi-burn-rate alerting straight to simple &lt;code&gt;docker-compose.yml&lt;/code&gt; services:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;payment-api&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;hashicorp/http-echo:latest&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;gbnt.slo.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;gbnt.slo.target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;99.9"&lt;/span&gt;
      &lt;span class="na"&gt;gbnt.slo.window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30d"&lt;/span&gt;
      &lt;span class="na"&gt;gbnt.slo.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caddy-http"&lt;/span&gt;
      &lt;span class="na"&gt;gbnt.slo.journey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Checkout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Flow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What makes Gubernator's SLO Suite unique?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Multi-Burn-Rate Alerting&lt;/strong&gt;: Automatically generates standard 4-window Prometheus recording and alert rules (&lt;strong&gt;Critical Page 1h/6h&lt;/strong&gt; &amp;amp; &lt;strong&gt;Warning Ticket 3d/14d&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic "No-Code" Management&lt;/strong&gt;: Click &lt;strong&gt;"+ Configure / Add SLO"&lt;/strong&gt; in the Web UI or call &lt;code&gt;POST /v1/slo/edit&lt;/code&gt; to create, edit, or disable SLOs on the fly without editing Compose files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite User Journeys&lt;/strong&gt;: Group multi-service SLOs into end-to-end flows (&lt;em&gt;Checkout Flow: API Gateway + Payment + DB&lt;/em&gt;) and automatically identify the weakest-link &lt;strong&gt;bottleneck service&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Correlation Timeline&lt;/strong&gt;: Cross-reference real-time burn rate spikes against stack updates and container restarts to answer &lt;em&gt;"Did our last deploy burn the budget?"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PromQL Dry-Run Backtesting&lt;/strong&gt;: Validate Compose YAML syntax and test PromQL queries against historical Prometheus metrics prior to deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Grafana Dashboards&lt;/strong&gt;: Automatically generates &lt;code&gt;/data/monitor/grafana/dashboards/slo_dashboard.json&lt;/code&gt; on rule sync.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  SLO Ecosystem Comparison Matrix
&lt;/h2&gt;

&lt;p&gt;Here is how Gubernator compares to other popular open-source and commercial SLO tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature / Capability&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Gubernator&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Sloth&lt;/strong&gt; (&lt;code&gt;slok/sloth&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Pyrra&lt;/strong&gt; (&lt;code&gt;pyrra-dev/pyrra&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;OpenSLO / Nobl9&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native Runtime Environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Docker Compose / Swarm / Bare Metal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes / OpenSLO CLI&lt;/td&gt;
&lt;td&gt;Kubernetes CRDs / Filesystem&lt;/td&gt;
&lt;td&gt;Multi-Cloud / SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Declarative Spec&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker-compose.yml&lt;/code&gt; labels (&lt;code&gt;gbnt.slo.*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;K8s CRDs / Sloth YAML&lt;/td&gt;
&lt;td&gt;Custom Resources (&lt;code&gt;ServiceLevelObjective&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;OpenSLO YAML Spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SRE Calculation Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Google SRE Multi-Burn-Rate&lt;/strong&gt; (via Sloth Engine)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Google SRE Multi-Burn-Rate&lt;/strong&gt; (4 windows)&lt;/td&gt;
&lt;td&gt;Prometheus Multi-Burn-Rate&lt;/td&gt;
&lt;td&gt;Proprietary / Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integrated Web Dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Flutter Web 5-Tab Suite)&lt;/td&gt;
&lt;td&gt;No (CLI / Operator only)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (React/Go UI)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (SaaS Console)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic Hot-Editing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Web UI Modal &amp;amp; REST API)&lt;/td&gt;
&lt;td&gt;No (Requires re-applying YAMLs)&lt;/td&gt;
&lt;td&gt;No (Read-only from K8s/Files)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (SaaS Console)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Journeys (Composite SLOs)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Aggregation &amp;amp; Bottleneck Analysis)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Related Services)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Correlation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Real-time Timeline of Stacks/Restarts)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial (CI/CD Webhooks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in SLI Templates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (&lt;code&gt;caddy-http&lt;/code&gt;, &lt;code&gt;http-status&lt;/code&gt;, &lt;code&gt;latency-p99&lt;/code&gt;, &lt;code&gt;grpc&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Partial (Sloth Libraries)&lt;/td&gt;
&lt;td&gt;No (Raw PromQL)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dry-Run PromQL Backtesting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Pre-deploy Validation)&lt;/td&gt;
&lt;td&gt;Partial (&lt;code&gt;validate&lt;/code&gt; command)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RED Metrics Breakdown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (RPS, Error Rate, P99 Latency Cards)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial (RPS &amp;amp; Errors)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automated Grafana Provisioning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (Auto-generates &lt;code&gt;slo_dashboard.json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Partial (Generic Rules)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Native CoreDNS 4-Tab Suite &amp;amp; Interactive Dig Playground
&lt;/h2&gt;

&lt;p&gt;Internal container service discovery should "just work." In Gubernator, every deployed container automatically receives &lt;code&gt;--dns &amp;lt;CoreDNS_IP&amp;gt;&lt;/code&gt;, enabling seamless &lt;code&gt;*.gbnt&lt;/code&gt; internal resolution across multi-node clusters.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;v2.13.0&lt;/strong&gt;, we are expanding CoreDNS into a full &lt;strong&gt;4-Tab Management Suite&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------------------+
|                      GUBERNATOR COREDNS SUITE                           |
|                                                                         |
|  [Tab 1: Auto-Discovered] [Tab 2: Custom Records] [Tab 3: DNS Playground] [Tab 4: Config]
+-------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tab 1: Auto-Discovered Stacks (&lt;code&gt;*.gbnt&lt;/code&gt;)&lt;/strong&gt;: Real-time table mapping running containers to &lt;code&gt;&amp;lt;service&amp;gt;.&amp;lt;stack&amp;gt;.gbnt&lt;/code&gt; with copyable &lt;code&gt;curl&lt;/code&gt; commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab 2: Custom Static DNS Records&lt;/strong&gt;: Manage custom &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;CNAME&lt;/code&gt;, &lt;code&gt;TXT&lt;/code&gt;, and &lt;code&gt;PTR&lt;/code&gt; records stored in SQLite and merged into CoreDNS on the fly (&lt;code&gt;POST /v1/coredns/custom-records&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab 3: Interactive Dig Playground&lt;/strong&gt;: A built-in terminal console to run DNS queries against local CoreDNS (&lt;code&gt;127.0.0.1:5354&lt;/code&gt;), benchmark query latency in milliseconds, and inspect raw &lt;code&gt;nslookup&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab 4: Upstream Forwarders &amp;amp; Corefile Editor&lt;/strong&gt;: One-click upstream DNS presets (&lt;strong&gt;Cloudflare 1.1.1.1&lt;/strong&gt;, &lt;strong&gt;Google 8.8.8.8&lt;/strong&gt;, &lt;strong&gt;Quad9 9.9.9.9&lt;/strong&gt;) and a live Corefile editor with container reload.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Caddy Ingress Suite &amp;amp; Zero-Trust Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;Gubernator packages &lt;strong&gt;Caddy&lt;/strong&gt; as its default edge proxy, handling HTTPS certificate provisioning, reverse proxying, and access logging across multi-node setups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features in the Caddy Suite:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;7-Tab Management Visualizer&lt;/strong&gt;: Dashboard, Dynamic Routes Matrix, Corefile Preview, TLS Certs Inspector, Real-time Access Logs, Log Config, and Prometheus Metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root CA Trust Installation&lt;/strong&gt;: One-click download of Gubernator's internal Root CA certificate for local TLS trust across your developer devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Ingress Label Routing&lt;/strong&gt;: Simply add &lt;code&gt;ingress.host=my-app.example.com&lt;/code&gt; to your Compose service, and Gubernator reconfigures Caddy route matrices across all cluster nodes automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started in Under 60 Seconds
&lt;/h2&gt;

&lt;p&gt;You can spin up a complete Gubernator cluster with full observability, CoreDNS, Caddy, and Prometheus/Grafana in seconds:&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;# 1. Download binary &amp;amp; start Gubernator Manager&lt;/span&gt;
curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/mario-ezquerro/gubernator/main/install.sh | bash
gbnt serve

&lt;span class="c"&gt;# 2. Deploy the SRE Monitoring Stack (Prometheus, Grafana, Loki, cAdvisor, Jaeger)&lt;/span&gt;
gbnt monitor init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  3. Access Web Dashboards
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Web UI Dashboard -&amp;gt; &lt;a href="http://localhost:4001" rel="noopener noreferrer"&gt;http://localhost:4001&lt;/a&gt;
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Grafana           -&amp;gt; &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;
&lt;/h1&gt;

&lt;h1&gt;
  
  
  CoreDNS Playground -&amp;gt; &lt;a href="http://localhost:4001" rel="noopener noreferrer"&gt;http://localhost:4001&lt;/a&gt; (CoreDNS tab)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


---

## Built Autonomous with Google Antigravity

A special shoutout to **Google Antigravity (AGY)**! The entire architecture of Gubernator -- from Go backend REST APIs, SQLite ORMs, Caddy route management, CoreDNS hosts sync, Sloth SLO rule compilation, down to the 5-tab Flutter Web UI -- was built autonomously in pair-programming sessions with Google Antigravity AI.

---

## Conclusion &amp;amp; Open Source

Gubernator aims to make container orchestration **fast, resilient, and enjoyable** again -- without the steep operational overhead of Kubernetes.

- **GitHub Repository**: [mario-ezquerro/gubernator](https://github.com/mario-ezquerro/gubernator)
- **Documentation &amp;amp; Guides**: [https://mario-ezquerro.github.io/gubernator/](https://mario-ezquerro.github.io/gubernator/)
- **Give us a Star**: If you find Gubernator useful, drop a star on GitHub!

*What are your thoughts on native SLO tracking for Docker Compose? Let us know in the comments below!*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>sre</category>
      <category>go</category>
    </item>
    <item>
      <title>Serving Gemma 4 E2B on a TPU v6e-1: what Trillium buys, and what it doesn't</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:25:31 +0000</pubDate>
      <link>https://dev.to/gde/serving-gemma-4-e2b-on-a-tpu-v6e-1-what-trillium-buys-and-what-it-doesnt-5691</link>
      <guid>https://dev.to/gde/serving-gemma-4-e2b-on-a-tpu-v6e-1-what-trillium-buys-and-what-it-doesnt-5691</guid>
      <description>&lt;h1&gt;
  
  
  Serving Gemma 4 E2B on a TPU v6e-1
&lt;/h1&gt;

&lt;p&gt;A Cloud TPU v6e-1 (Trillium) costs &lt;strong&gt;2.25×&lt;/strong&gt; a v5e-1 and returns &lt;strong&gt;1.62–1.68×&lt;/strong&gt; the throughput on workloads that fit in a v5e, and &lt;strong&gt;2.32–2.77×&lt;/strong&gt; on workloads that do not. Per output token that makes v6e &lt;strong&gt;34–39% dearer&lt;/strong&gt; in the first regime and &lt;strong&gt;3–19% cheaper&lt;/strong&gt; in the second — so the case for the bigger chip is narrower than the memory ratio suggests, and break-even sits at roughly 270,000 KV tokens.&lt;/p&gt;

&lt;p&gt;v6e is not a general upgrade over v5e. It is a &lt;strong&gt;memory&lt;/strong&gt; upgrade sold at a compute price: 32 GB against 16, a KV pool of &lt;strong&gt;1,151,744 tokens against 321,376 (3.6×)&lt;/strong&gt;, for &lt;strong&gt;1.907×&lt;/strong&gt; the bandwidth. Where the extra memory does nothing, the workload pays 2.25× for 1.6×.&lt;/p&gt;

&lt;p&gt;Two findings drive the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no capacity knee at any occupancy tested.&lt;/strong&gt; &lt;code&gt;TTFT = −8542 + 265 × concurrency&lt;/code&gt;, R² = &lt;strong&gt;0.999996&lt;/strong&gt;, across &lt;strong&gt;56% to 157%&lt;/strong&gt; of the KV pool, with &lt;code&gt;num_preemptions_total = 0&lt;/code&gt; in every cell. A line fitted entirely below 100% occupancy predicts 157% to within 0.13%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spot is more expensive than flex-start on this chip&lt;/strong&gt; — $1.4033 against $1.35/chip-hr in us-east5, reversing the v5e ordering. The cheaper option is also the preemption-free one that stops billing by itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration:&lt;/strong&gt; &lt;code&gt;v6e-1&lt;/code&gt; (&lt;code&gt;ct6e-standard-1t&lt;/code&gt;, one Trillium chip), &lt;code&gt;vllm/vllm-tpu:nightly&lt;/code&gt;, vLLM &lt;code&gt;0.26.1rc1.dev256+gf5bb701fa&lt;/code&gt;, tpu-inference JAX backend, &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; at bf16, TP=1, &lt;code&gt;max_model_len&lt;/code&gt; 32768, &lt;code&gt;max_num_batched_tokens&lt;/code&gt; 4096, &lt;code&gt;kv_cache_dtype=auto&lt;/code&gt;, prefix caching on. &lt;code&gt;OUTPUT_LEN&lt;/code&gt; 128 throughout. v5e-1 comparison figures are from the same model and engine family on &lt;code&gt;v5litepod-1&lt;/code&gt; and are not a controlled A/B — read them as shape, not delta.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — Getting a v6e-1 at all
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 The gcloud spelling table
&lt;/h3&gt;

&lt;p&gt;On v5e, &lt;code&gt;v5e&lt;/code&gt; is spelled &lt;strong&gt;&lt;code&gt;v5litepod&lt;/code&gt;&lt;/strong&gt; to gcloud. On v6e the marketing name and the CLI value coincide — which teaches a habit that breaks on the next chip.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;v5e single chip&lt;/th&gt;
&lt;th&gt;v6e single chip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prose, directory names&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v5e-1&lt;/code&gt; / &lt;code&gt;v5e1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v6e-1&lt;/code&gt; / &lt;code&gt;v6e1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--accelerator-type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;v5litepod-1&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v6e-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex-start runtime version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2-alpha-tpuv5-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;v2-alpha-tpuv6e&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--type&lt;/code&gt; / &lt;code&gt;--topology&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v5litepod&lt;/code&gt; / &lt;code&gt;1x1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v6e&lt;/code&gt; / &lt;code&gt;1x1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU API quota id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TPUV5sLitepodPerProjectPerZoneForTPUAPI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TPUV6EPerProjectPerZoneForTPUAPI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot quota id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TPUV5sPreemptibleLitepodPerProjectPerZoneForTPUAPI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TPUV6EPreemptiblePerProjectPerZoneForTPUAPI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nothing in that table survives a retarget by analogy.&lt;/strong&gt; The v6e quota ids drop the &lt;code&gt;Litepod&lt;/code&gt; the v5e ids carry, and a stale quota id fails &lt;em&gt;quietly&lt;/em&gt; — it matches no rows rather than erroring, producing a confident "no quota anywhere" that is a typo. &lt;code&gt;v6e1&lt;/code&gt;, the directory spelling without the hyphen, is still not a valid gcloud value even though &lt;code&gt;v6e&lt;/code&gt; is.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Provisioning clears three independent gates
&lt;/h3&gt;

&lt;p&gt;A creation must pass three separate checks. They fail differently, and the one that is easiest to query carries the least information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 1 — does the zone have &lt;code&gt;v6e-1&lt;/code&gt; hardware?&lt;/strong&gt; Of 37 zones reporting quota, only &lt;strong&gt;18&lt;/strong&gt; offer the accelerator type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute tpus accelerator-types list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"type=v6e-1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google's regions-and-zones page names 8, a strict subset of what the API accepts. Read the API. This gate is provisioning-model-independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 2 — does that zone offer that provisioning model for that accelerator type?&lt;/strong&gt; Independent of both quota and hardware, and where a published price stops meaning anything. &lt;strong&gt;us-central1-b and us-south1-a have v6e-1 hardware, quota, and a published &lt;code&gt;DWS Defined Duration V6e&lt;/code&gt; rate for their region, and both reject flex-start at the API:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FLEX_START provisioning model is not supported for accelerator type "v6e-1" in location "us-central1-b"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed accepting flex-start: &lt;strong&gt;us-east5-a, us-east5-b, europe-west4-a&lt;/strong&gt;. This is the v6e analogue of the v5e result, where flex-start &lt;code&gt;v5litepod-1&lt;/code&gt; was accepted in exactly one zone out of 44. Note that europe-west4 &lt;strong&gt;inverts across generations&lt;/strong&gt;: it rejected &lt;code&gt;v5litepod-1&lt;/code&gt; while quoting a rate for it, and accepts &lt;code&gt;v6e-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 3 — is there free capacity right now?&lt;/strong&gt; Reachable only after the first two pass, and the one gate that is not a property of the zone. Requests in accepting zones sit at &lt;code&gt;WAITING_FOR_RESOURCES&lt;/code&gt; for tens of minutes to hours before capacity is granted. &lt;strong&gt;That state is not a failure&lt;/strong&gt; — it should not be recorded as one, and the request should not be torn down, because flex-start capacity can take up to two hours to come back once dropped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A published rate is not an offer of capacity, and not even an offer of the &lt;em&gt;provisioning model&lt;/em&gt;.&lt;br&gt;
Quota is the first thing most people check and the last thing that should reassure them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.3 Provision
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Flex-start, via the Queued Resource API — the only model that accepts --max-run-duration,&lt;/span&gt;
&lt;span class="c"&gt;# i.e. the only one that stops billing on its own.&lt;/span&gt;
gcloud alpha compute tpus queued-resources create gemma4-v6e &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma4-v6e-node &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east5-b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--accelerator-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v6e-1 &lt;span class="nt"&gt;--runtime-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v2-alpha-tpuv6e &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;flex-start &lt;span class="nt"&gt;--max-run-duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4h

&lt;span class="c"&gt;# Spot — preemptible with ~30s notice, no run limit, and on this chip not the cheap option.&lt;/span&gt;
gcloud alpha compute tpus tpu-vm create gemma4-v6e &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east5-b &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v6e &lt;span class="nt"&gt;--topology&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1x1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spot &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v2-alpha-tpuv6e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--max-run-duration&lt;/code&gt; is flex-start-only. &lt;code&gt;--valid-until-duration&lt;/code&gt; bounds the &lt;em&gt;request&lt;/em&gt;, not the run, so it is shared by all three models. Spot and on-demand nodes bill until preempted or deleted.&lt;/p&gt;

&lt;p&gt;The Hugging Face token belongs in Secret Manager, not in the startup script — the rendered script is uploaded as &lt;strong&gt;instance metadata&lt;/strong&gt;, and anything baked into it is readable from the instance:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"hf_xxxxxxxxxxxx"&lt;/span&gt; | gcloud secrets create hf-token &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;- &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.4 Serve
&lt;/h3&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;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; vllm-gemma4 &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="nt"&gt;--net&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; /dev/shm:/dev/shm &lt;span class="nt"&gt;--shm-size&lt;/span&gt; 10gb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ~/.cache/vllm:/root/.cache/vllm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;HF_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/shm &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;HF_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.hf_token&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  vllm/vllm-tpu:nightly &lt;span class="se"&gt;\&lt;/span&gt;
  vllm serve google/gemma-4-E2B-it &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kv-cache-dtype&lt;/span&gt; auto &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 32768 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--max-num-batched-tokens&lt;/span&gt; 4096 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enable-prefix-caching&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--disable-chunked-mm-input&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--limit-mm-per-prompt&lt;/span&gt; &lt;span class="s1"&gt;'{"image":4,"audio":1}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enable-auto-tool-choice&lt;/span&gt; &lt;span class="nt"&gt;--tool-call-parser&lt;/span&gt; gemma4 &lt;span class="nt"&gt;--reasoning-parser&lt;/span&gt; gemma4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deliberate differences from the equivalent v5e configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--gpu-memory-utilization&lt;/code&gt; is absent.&lt;/strong&gt; On v5e, 0.92 was a ceiling: 0.95 died after a 691-second compile while loading &lt;code&gt;jit_structured_decode_fn&lt;/code&gt;, because compiled XLA programs live &lt;em&gt;outside&lt;/em&gt; the knob's control. That is a 16 GB result with no bearing on 32 GB, and v6e's ceiling is unmeasured, so the flag is left underived rather than carrying another chip's limit forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-model-len&lt;/code&gt; is 32768&lt;/strong&gt;, not the 16384 typical of v5e-era configs. §2.3 shows it is free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify against the boot log, not against the flags:&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;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; vllm-gemma4 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"Memory statistics|GPU KV cache size|block_size"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory statistics | total_hbm_limit_gb=31.24GiB | total_hbm_limit_cap_gb=28.74GiB
                  | total_hbm_used_gb=8.97GiB   | total_hbm_avail_gb=19.77GiB
GPU KV cache size: 1,151,744 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smoke-test on &lt;strong&gt;&lt;code&gt;/v1/chat/completions&lt;/code&gt;&lt;/strong&gt;. Raw &lt;code&gt;/v1/completions&lt;/code&gt; returns an empty string on &lt;code&gt;-it&lt;/code&gt; models, which looks exactly like a broken deploy and is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — The chip
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 On paper
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spec (per chip)&lt;/th&gt;
&lt;th&gt;v5e&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e (Trillium)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HBM capacity&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HBM bandwidth&lt;/td&gt;
&lt;td&gt;800 GiBps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,638 GBps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.907×&lt;/strong&gt; — units differ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak bf16&lt;/td&gt;
&lt;td&gt;197 TFLOPs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;918 TFLOPs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.66×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak Int8&lt;/td&gt;
&lt;td&gt;393 TOPs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,836 TOPs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.67×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TensorCores&lt;/td&gt;
&lt;td&gt;1 (4 MXUs, 128×128)&lt;/td&gt;
&lt;td&gt;1 (MXUs 256×256, count unresolved)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICI&lt;/td&gt;
&lt;td&gt;400 GBps bidi, 4 ports&lt;/td&gt;
&lt;td&gt;800 GBps bidi, 4 ports&lt;/td&gt;
&lt;td&gt;2.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ct5lp-hightpu-1t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ct6e-standard-1t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-demand list&lt;/td&gt;
&lt;td&gt;~$1.20/chip-hr&lt;/td&gt;
&lt;td&gt;~$2.70/chip-hr&lt;/td&gt;
&lt;td&gt;2.25×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The units trap costs 7%.&lt;/strong&gt; Google quotes v5e HBM bandwidth in &lt;strong&gt;GiBps&lt;/strong&gt; and v6e in &lt;strong&gt;GBps&lt;/strong&gt; — on the v5e page, in the same table that uses GBps for ICI. Normalised, 800 GiBps = 858.99 GB/s, making the true ratio &lt;strong&gt;1.907×&lt;/strong&gt; rather than the 2.047× obtained by dividing the printed figures. The launch-blog claim that Trillium "doubled" HBM bandwidth is the naive reading. For bandwidth-bound work — decode is bandwidth-bound — that 7% separates a ratio that explains the measurement from one that does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shape trap:&lt;/strong&gt; 2.25× the price for 2× memory, ~1.9× bandwidth, and &lt;strong&gt;4.7× the raw FLOPS&lt;/strong&gt;. The 4.7× only pays for prefill-heavy or long-context work that burns the matrix units. For pure decode, v5e is priced close to right and v6e is not.&lt;/p&gt;

&lt;p&gt;One row not to build on: Google's v6e page states each TensorCore has &lt;strong&gt;2&lt;/strong&gt; MXUs, but two 256×256 arrays is exactly 2× v5e's four 128×128, against a published peak of &lt;strong&gt;4.66×&lt;/strong&gt; — which would require a 2.33× clock increase on top. Four 256×256 closes it almost exactly (262,144 MACs × 2 flops × 1.75 GHz = 917.5 TFLOPs against a published 918). The &lt;strong&gt;918 figure is sound&lt;/strong&gt;, cross-checking against the same page's 234.9 PFLOPs-per-Pod row. Treat peak compute as reliable and the MXU count as unresolved.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 The memory budget
&lt;/h3&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;v5e-1&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e-1&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total HBM visible&lt;/td&gt;
&lt;td&gt;15.75 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31.24 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocation cap&lt;/td&gt;
&lt;td&gt;14.49 GiB (at 0.92)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.74 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B weights, resident&lt;/td&gt;
&lt;td&gt;8.97 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.97 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache pool&lt;/td&gt;
&lt;td&gt;5.52 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.77 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KV tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;321,376&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,151,744&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3.58× the KV capacity for 2.25× the price.&lt;/strong&gt; The weights are identical — E2B costs 8.97 GiB wherever it runs, consuming &lt;strong&gt;62% of a v5e's usable budget and 31% of a v6e's&lt;/strong&gt;. Every byte of the difference goes to KV.&lt;/p&gt;

&lt;p&gt;The arithmetic closes independently on both chips, which is what distinguishes a real allocation from a log line: 19.77 GiB ÷ 18,432 B/token = 1,151,686, within &lt;strong&gt;0.005%&lt;/strong&gt; of the measured 1,151,744. The same division reproduces the v5e figure to 0.06%.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Longer context is free
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;max_model_len&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;block_size&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;KV pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16,384&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1,151,776 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,151,744 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1,151,744 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A 0.003% spread across a 4× range.&lt;/strong&gt; The Pallas backend derives &lt;code&gt;block_size&lt;/code&gt; to hold blocks-per-request constant at 512, so doubling the context doubles the page size, halves the block count, and arrives at the same token capacity. The same behaviour holds on v5e at a quarter of the pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never set &lt;code&gt;--block-size&lt;/code&gt;.&lt;/strong&gt; Pinning it fights the derivation that keeps long context free.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.4 Data types
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;format&lt;/th&gt;
&lt;th&gt;native in the MXU?&lt;/th&gt;
&lt;th&gt;v5e&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;int8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;2× bf16&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;the only compute win&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;still the only compute win&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;fp8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;storage/bandwidth only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;❌ — unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int4 / fp4&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;footprint only&lt;/td&gt;
&lt;td&gt;footprint only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Trillium does not bring fp8. &lt;strong&gt;Google's v6e page publishes exactly three peak-compute rows — &lt;code&gt;bf16: 918 TFLOPs&lt;/code&gt;, &lt;code&gt;Int8: 1836 TOPs&lt;/code&gt;, and a per-Pod bf16 figure — and no fp8 row anywhere.&lt;/strong&gt; The int8 figure being &lt;em&gt;exactly&lt;/em&gt; 2× bf16 is the signature of a native MXU path; the absent fp8 row is the tell in the other direction. &lt;strong&gt;v7/Ironwood is the first TPU with fp8 in the matrix units&lt;/strong&gt; — no conclusion in this section carries forward to it.&lt;/p&gt;

&lt;p&gt;The consequence is observable at boot. With &lt;code&gt;--kv-cache-dtype auto&lt;/code&gt; — the flag never passed — the engine logs this &lt;strong&gt;twenty times&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Automatically using fp8_e5m2 for FP8 KV cache on TPU v6e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and allocates &lt;code&gt;regular_attn_dtype=bfloat16&lt;/code&gt;. The arithmetic is not close: 1,151,744 tokens against 19.77 GiB is the &lt;strong&gt;bf16&lt;/strong&gt; model to 0.01%, while the fp8 model is &lt;strong&gt;50%&lt;/strong&gt; off. The two hypotheses are far apart, making this a discriminator rather than a tolerance argument.&lt;/p&gt;

&lt;p&gt;That is the sixth false fp8 signal recorded on this stack and the first on a second silicon generation. On v5e, &lt;code&gt;--kv-cache-dtype fp8_e4m3&lt;/code&gt; was accepted at the CLI, echoed in &lt;code&gt;non-default args&lt;/code&gt;, praised by a log line, reported in &lt;code&gt;/metrics&lt;/code&gt;, and allocated a genuinely &lt;code&gt;float8_e4m3fn&lt;/code&gt; tensor — five independent signals of success — for a &lt;strong&gt;1.000×&lt;/strong&gt; capacity ratio, because the KV block layout is word-aligned: as the element width halves, the shape goes &lt;code&gt;(32,1,2,256) → (32,1,4,256)&lt;/code&gt; and the byte count never moves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verify quantization from the boot allocation arithmetic — never from the flag being accepted, and&lt;br&gt;
never from engine prose.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What v6e plausibly does unblock: on v5e, qwix int8 &lt;strong&gt;weight&lt;/strong&gt; quantization died at &lt;code&gt;RESOURCE_EXHAUSTED: HLO temporaries (16.23G) exceeds available HBM (15.75G)&lt;/code&gt;, short by 0.48 G. v6e has 31.24 GiB, roughly 15 GiB of headroom over that same temporary peak. That failure was an HBM ceiling and this chip doubles it. Untested, and it fails fast — 2.5–4 minutes if it still does not boot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Six properties of Gemma 4 E2B that decide the rest
&lt;/h2&gt;

&lt;p&gt;None of these change with the chip, but their consequences land differently on 32 GB.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_hidden_layers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;35 (&lt;strong&gt;28 sliding / 7 full&lt;/strong&gt;, &lt;code&gt;i % 5 == 4&lt;/code&gt; is full)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_kv_shared_layers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;20&lt;/strong&gt; — only &lt;strong&gt;15 layers own a cache&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;num_attention_heads&lt;/code&gt; / &lt;code&gt;num_key_value_heads&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;8 / &lt;strong&gt;1&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;head_dim&lt;/code&gt; / &lt;code&gt;global_head_dim&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;256 / 512&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hidden_size&lt;/code&gt; / &lt;code&gt;intermediate_size&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1536 / 6144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vocab_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;262,144 (tied embeddings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sliding_window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resident at bf16&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.97 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;1. "E2B" is not a 2B model.&lt;/strong&gt; ~2B &lt;em&gt;effective&lt;/em&gt; against ~5B total, landing at &lt;strong&gt;8.97 GiB&lt;/strong&gt; resident. The &lt;code&gt;E&lt;/code&gt; prefix is load-bearing: reading &lt;code&gt;E4B&lt;/code&gt; as "a 4B model" understates its weights by roughly 2×, exactly the difference between fitting a 16 GB chip and not. On v6e this matters less for E2B than for what else becomes possible — &lt;strong&gt;E4B fits at bf16 here and does not on v5e.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. There are two attention geometries.&lt;/strong&gt; Sliding layers run at &lt;code&gt;head_dim&lt;/code&gt; 256; the seven full-attention layers run at &lt;strong&gt;512&lt;/strong&gt;, applying to K and V, not just Q. Reading a single &lt;code&gt;head_dim&lt;/code&gt; and applying it to all 35 layers under-counts the full layers by 2× — a 17% KV sizing error. It is also the root cause of the 2.9× capacity tax in Part 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Twenty of the thirty-five layers read another layer's cache.&lt;/strong&gt; &lt;code&gt;first_shared = 35 − 20 = 15&lt;/code&gt;, so layers 0–14 own KV and 15–34 share. The rule is "last preceding layer of the same attention type", and within 0–14 that means &lt;strong&gt;all twenty shared layers resolve to two source caches&lt;/strong&gt; — layer 13 for the sliding ones, layer 14 for the full ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. KV costs 18 KiB/token, and the boot log misreports why.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 sliding cached layers × 1 KV head × 2 (K,V) × 256 × 2 B = 12,288 B
 3 full    cached layers × 1 KV head × 2 (K,V) × 512 × 2 B =  6,144 B
                                                    total  = 18,432 B = 18 KiB/token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line describing the cache, &lt;code&gt;regular_attn_shape=(num_blocks, (64, 1, 2, 256))&lt;/code&gt;, is a &lt;strong&gt;first-wins sample taken from layer 0&lt;/strong&gt;, which is sliding, hence 256. It says nothing about layers 4, 9 and 14: &lt;code&gt;count&lt;/code&gt; increments for all 15 tensors while &lt;code&gt;shape&lt;/code&gt; is written once and never updated. The allocation is correct; the line is misleading. &lt;strong&gt;Size KV from the config geometry and check it against &lt;code&gt;total_hbm_avail_gb&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. One KV head means more chips make things worse.&lt;/strong&gt; &lt;code&gt;num_key_value_heads = 1&lt;/code&gt; is full MQA, and a single head &lt;strong&gt;cannot be sharded&lt;/strong&gt; — runtimes pad &lt;code&gt;num_kv_heads&lt;/code&gt; up to a multiple of the TP size, so at TP=4 the same head is replicated at &lt;strong&gt;4× the KV memory&lt;/strong&gt;. A larger topology multiplies this model's KV cost rather than dividing it. &lt;strong&gt;The answer to "E2B needs more memory" is a bigger chip, not more chips.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The heads do not tile the hidden size.&lt;/strong&gt; &lt;code&gt;8 × 256 = 2048&lt;/code&gt; against &lt;code&gt;hidden_size = 1536&lt;/code&gt;, so the Q projection is rectangular. Code computing &lt;code&gt;head_dim = hidden_size / num_heads&lt;/code&gt; gets &lt;strong&gt;192&lt;/strong&gt; and is silently wrong.&lt;/p&gt;

&lt;p&gt;One further property explains performance rather than memory: &lt;strong&gt;4.38 GiB of the 8.97 GiB resident is per-layer embedding tables&lt;/strong&gt; (262,144 × 256 × 35), which are &lt;em&gt;gathered per token, not streamed&lt;/em&gt;. Only ~3.15 GiB moves per decode step, which is why an 8.97 GiB model decodes as fast as it does, and it sets the bandwidth floor used in Part 4.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — Results
&lt;/h2&gt;

&lt;p&gt;Roles are sized against the v6e pool: &lt;code&gt;control&lt;/code&gt; fits both chips trivially, &lt;code&gt;bandwidth&lt;/code&gt; fits both but moves substantial KV per step, &lt;code&gt;v6e_only&lt;/code&gt; exceeds v5e's entire pool, and &lt;code&gt;long_ctx&lt;/code&gt; requires &lt;code&gt;max_model_len &amp;gt; 16384&lt;/code&gt; — impossible on v5e at any setting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ctx&lt;/th&gt;
&lt;th&gt;clients&lt;/th&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;KV needed&lt;/th&gt;
&lt;th&gt;v5e tok/s&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e tok/s&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per-stream&lt;/th&gt;
&lt;th&gt;median TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;control&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;123.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;202.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.65×&lt;/td&gt;
&lt;td&gt;202.9&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;control&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;738.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,195.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.62×&lt;/td&gt;
&lt;td&gt;149.4&lt;/td&gt;
&lt;td&gt;26 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;control&lt;/td&gt;
&lt;td&gt;18,432&lt;/td&gt;
&lt;td&gt;896.1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,508.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.68×&lt;/td&gt;
&lt;td&gt;94.2&lt;/td&gt;
&lt;td&gt;149 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;bandwidth&lt;/td&gt;
&lt;td&gt;270,336&lt;/td&gt;
&lt;td&gt;585.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,360.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.32×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21.3&lt;/td&gt;
&lt;td&gt;303 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;bandwidth&lt;/td&gt;
&lt;td&gt;266,240&lt;/td&gt;
&lt;td&gt;307.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;758.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.46×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23.7&lt;/td&gt;
&lt;td&gt;348 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;v6e_only&lt;/td&gt;
&lt;td&gt;532,480&lt;/td&gt;
&lt;td&gt;314.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;870.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.77×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.6&lt;/td&gt;
&lt;td&gt;1,006 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;v6e_only&lt;/td&gt;
&lt;td&gt;516,096&lt;/td&gt;
&lt;td&gt;166.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;432.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.59×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.5&lt;/td&gt;
&lt;td&gt;3,276 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;v6e_only&lt;/td&gt;
&lt;td&gt;1,032,192&lt;/td&gt;
&lt;td&gt;166.7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;446.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.68×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;8,459 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;long_ctx&lt;/td&gt;
&lt;td&gt;514,048&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;242.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;15.2&lt;/td&gt;
&lt;td&gt;2,233 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;long_ctx&lt;/td&gt;
&lt;td&gt;1,028,096&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;229.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;8,760 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is correctly no v5e reference for the &lt;code&gt;long_ctx&lt;/code&gt; cells: that configuration cannot exist on v5e.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A caution about the 4,096–8,192 band.&lt;/strong&gt; Those three cells are the most sensitive in the matrix to how a sweep is ordered. Run at a shared &lt;code&gt;--seed&lt;/code&gt; after a longer-context cell, they report &lt;strong&gt;12–19% higher&lt;/strong&gt; than they do with a distinct seed per cell, while every cell at 128, 1,024, 16,000 and 32,000 tokens moves by under 6.3% either way. The figures above are the clean-seed ones. Anything quoting this band from a single-seed sweep is quoting the high side.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 The asymmetry
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;regime&lt;/th&gt;
&lt;th&gt;cells&lt;/th&gt;
&lt;th&gt;mean vs v5e&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;working set &amp;lt; 10% of v5e's pool&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.65×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;working set ≥ 83% of v5e's pool&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.56×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against a &lt;strong&gt;1.907×&lt;/strong&gt; bandwidth ratio and a &lt;strong&gt;2.25×&lt;/strong&gt; price ratio.&lt;/p&gt;

&lt;p&gt;If v6e were simply faster, every cell would improve by roughly the same factor. Control cells move roughly with bandwidth and no more. Cells where v5e was over its pool — evicting and recomputing — move about 2.6×, because v6e is not doing that work. &lt;strong&gt;On decode throughput alone this chip is a poor deal. It pays for capacity, not speed&lt;/strong&gt; — and note that even the memory-bound mean of 2.56× only just clears the 2.25× price ratio.&lt;/p&gt;

&lt;p&gt;Single stream is the cleanest bandwidth read: &lt;strong&gt;TPOT 4.72 ms on v6e against 8.02 ms on v5e, 1.70×&lt;/strong&gt; on a 1.907× bandwidth ratio. Decode moves ~3.15 GiB per step (derived from layer geometry), which at 1,638 GB/s is a &lt;strong&gt;2.06 ms floor against 4.72 ms measured — 44% of peak&lt;/strong&gt;, slightly worse utilisation than v5e's 49%. Roughly 2× of headroom sits in fixed per-step cost on both chips, not in memory bandwidth.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;1.65×&lt;/strong&gt; control figure sits &lt;em&gt;below&lt;/em&gt; the bandwidth ratio and is unexplained. The MXU geometry change (4×128×128 → 256×256, a 4× larger minimum tile) is a plausible cause but is not demonstrated; separating it from "small batches do not saturate bandwidth" requires a batch-size sweep at fixed short context.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 There is no capacity knee
&lt;/h3&gt;

&lt;p&gt;A widely used v5e rule of thumb — keep &lt;code&gt;clients × context&lt;/code&gt; under ~78% of the pool — scales to ~900,000 tokens on v6e. Tested directly at fixed 16,000 context with only concurrency varying:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;clients&lt;/th&gt;
&lt;th&gt;KV needed&lt;/th&gt;
&lt;th&gt;% of pool&lt;/th&gt;
&lt;th&gt;tok/s&lt;/th&gt;
&lt;th&gt;median TTFT&lt;/th&gt;
&lt;th&gt;preemptions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;645,120&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;452.7&lt;/td&gt;
&lt;td&gt;2,088 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;741,888&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;465.9&lt;/td&gt;
&lt;td&gt;3,659 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;838,656&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;470.6&lt;/td&gt;
&lt;td&gt;5,273 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;903,168&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;78%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;467.7&lt;/td&gt;
&lt;td&gt;6,309 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;967,680&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;471.6&lt;/td&gt;
&lt;td&gt;7,373 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1,032,192&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;446.0&lt;/td&gt;
&lt;td&gt;8,459 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;1,161,216&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;101%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;458.5&lt;/td&gt;
&lt;td&gt;10,544 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;1,290,240&lt;/td&gt;
&lt;td&gt;112%&lt;/td&gt;
&lt;td&gt;459.1&lt;/td&gt;
&lt;td&gt;12,689 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;1,548,288&lt;/td&gt;
&lt;td&gt;134%&lt;/td&gt;
&lt;td&gt;472.0&lt;/td&gt;
&lt;td&gt;16,937 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;1,806,336&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;157%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;465.5&lt;/td&gt;
&lt;td&gt;21,229 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TTFT = −8542 + 265 × concurrency&lt;/code&gt;, R² = 0.999996 over all ten points.&lt;/strong&gt; Throughput is flat at 446.0–472.0 tok/s (&lt;strong&gt;5.8%&lt;/strong&gt;) across the range. TPOT sits at 66.2–67.4 ms and does not move. &lt;code&gt;num_preemptions_total&lt;/code&gt; is &lt;strong&gt;0 in every cell, including 157% occupancy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The line is the most reproducible result in the matrix. Re-measured on a separate node, the 90% and 157% points land at &lt;strong&gt;+0.18%&lt;/strong&gt; and &lt;strong&gt;+0.22%&lt;/strong&gt; of what it predicts, with zero preemptions in both.&lt;/p&gt;

&lt;p&gt;There is no knee at 78%, none anywhere in 56–157%, and &lt;strong&gt;crossing 100% of the pool is not an event&lt;/strong&gt;. A line fitted entirely below 100% predicts the 157% point to within 0.13%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scheduler admission-controls rather than evicts.&lt;/strong&gt; It admits what fits and queues the rest, so the working set never thrashes. Occupancy alone costs nothing; &lt;em&gt;eviction&lt;/em&gt; would, and it never engaged. The v5e rule was a queueing curve read as a memory cliff.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Size by the latency you will accept, not by pool occupancy.&lt;/strong&gt; Throughput saturates near&lt;br&gt;
concurrency 40, and every further concurrent request buys &lt;strong&gt;265 ms of TTFT and nothing else&lt;/strong&gt;. The&lt;br&gt;
pool bounds what is &lt;em&gt;resident&lt;/em&gt;; it is not a performance threshold.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.3 Two benchmarking traps on this stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. A config can silently fail to take effect, and the obvious check can miss it.&lt;/strong&gt; vLLM prints &lt;code&gt;max_model_len&lt;/code&gt; inside a dict repr (&lt;code&gt;'max_model_len': 16384&lt;/code&gt;), so a regex written for the bare key matches nothing and returns no result rather than a mismatch — indistinguishable from a pass unless the check distinguishes them. The reliable signal is a &lt;em&gt;different&lt;/em&gt; derived quantity downstream of the same setting: &lt;code&gt;block_size&lt;/code&gt; reads 32 at 16384 and 64 at 32768. &lt;strong&gt;Verify a setting through two independent derivations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;--seed 0&lt;/code&gt; plus prefix caching silently couples cells.&lt;/strong&gt; vLLM defaults to &lt;code&gt;enable_prefix_caching=True&lt;/code&gt;, and the random dataset is deterministic in the seed, so two cells at the same &lt;code&gt;input_len&lt;/code&gt; draw overlapping prompts and the later one is served from cache. A &lt;code&gt;16000×32&lt;/code&gt; cell run after &lt;code&gt;16000×64&lt;/code&gt; took &lt;strong&gt;1,539,904 cache-hit tokens against 1,536,000 input tokens&lt;/strong&gt; — essentially all of it — and its 2,461 ms TTFT is not comparable to anything.&lt;/p&gt;

&lt;p&gt;That artifact, combined with an unsampled gap between 46% and 89% of pool, is enough to manufacture an apparent 3.4× cliff: &lt;strong&gt;two points far apart with the lower one artificially fast reads as a cliff.&lt;/strong&gt; Vary the seed per cell; the knee and overflow sweeps above measured &lt;strong&gt;0.0% prefix hits throughout&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5 — Why there is no cliff, and what it costs
&lt;/h2&gt;

&lt;p&gt;E2B caches 15 of 35 layers: 12 sliding-window at head_dim 256 with a 512-token window, 3 full-attention at 512. tpu_inference sees two head dims, sets &lt;code&gt;disable_sliding_window&lt;/code&gt;, and gives &lt;strong&gt;every&lt;/strong&gt; layer full-length blocks — &lt;code&gt;num_kv_cache_groups=1&lt;/code&gt;, 15 tensors, confirmed in the boot log.&lt;/p&gt;

&lt;p&gt;Allocation and reads therefore diverge:&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;12 sliding layers&lt;/th&gt;
&lt;th&gt;3 full layers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;allocated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;full context — 12,288 B/token&lt;/td&gt;
&lt;td&gt;6,144 B/token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;read per decode step&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6.29 MB, constant&lt;/strong&gt; once L &amp;gt; 512&lt;/td&gt;
&lt;td&gt;98.3 MB at L = 16,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 16,000 context, &lt;strong&gt;94% of the KV bytes read come from 3 of the 15 cached layers.&lt;/strong&gt; The pool is charged ~18 KiB/token where ~6.4 KiB would suffice.&lt;/p&gt;

&lt;p&gt;This does not prevent a cliff — it moves the cliff closer. It is a capacity &lt;em&gt;tax&lt;/em&gt;, not a latency shield; no cliff appears because nothing is ever evicted.&lt;/p&gt;

&lt;p&gt;The tax is worth &lt;strong&gt;2.91×&lt;/strong&gt;: at 32,768 context, 576.0 MiB/seq allocated against 198.0 MiB/seq windowed. The trigger is &lt;code&gt;disable_sliding_window = len(head_size_set) &amp;gt; 1&lt;/code&gt;, and Gemma 4's 256/512 head-dim split trips it &lt;strong&gt;family-wide, at every size&lt;/strong&gt;. It is gated on an upstream &lt;code&gt;TODO&lt;/code&gt; and is not settable from the serving side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That single upstream fix is worth more than every available quantization flag combined — 2.91× the&lt;br&gt;
effective KV capacity at zero quality cost.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 6 — Cost
&lt;/h2&gt;

&lt;p&gt;Rates from the Cloud Billing Catalog, per chip-hour:&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;v5e (us-west4)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e (us-east5)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spot&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.5779&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$1.4033&lt;/td&gt;
&lt;td&gt;2.43×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex-start (DWS)&lt;/td&gt;
&lt;td&gt;$0.6000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.3500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.25×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-demand&lt;/td&gt;
&lt;td&gt;$1.2000&lt;/td&gt;
&lt;td&gt;$2.7000&lt;/td&gt;
&lt;td&gt;2.25×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;On v6e, spot is dearer than flex-start&lt;/strong&gt;, inverting the v5e ordering and the advice with it. On v5e, flex-start cost 3.8% more and bought preemption-freedom. On v6e flex-start is &lt;strong&gt;both cheaper and preemption-free&lt;/strong&gt;, and it self-terminates via &lt;code&gt;--max-run-duration&lt;/code&gt;, which the other two do not. There is no trade left to make.&lt;/p&gt;

&lt;p&gt;Two catalog naming traps: flex-start is sold as &lt;strong&gt;"DWS Defined Duration"&lt;/strong&gt; (Dynamic Workload Scheduler) and drops the &lt;code&gt;Tpu&lt;/code&gt; prefix (&lt;code&gt;DWS Defined Duration V6e&lt;/code&gt;), while spot is &lt;code&gt;usageType: Preemptible&lt;/code&gt; spelled &lt;code&gt;TpuV6e attached to Spot Preemptible VMs&lt;/code&gt;. The &lt;code&gt;Reserved …&lt;/code&gt;, &lt;code&gt;Commitment v1: …&lt;/code&gt; and &lt;strong&gt;&lt;code&gt;Capacity Optimized TpuV6e …&lt;/code&gt;&lt;/strong&gt; SKUs describe the same chip in the same region and three are also &lt;code&gt;OnDemand&lt;/code&gt;, so anchor the match patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per million output tokens, flex-start both sides
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ctx&lt;/th&gt;
&lt;th&gt;clients&lt;/th&gt;
&lt;th&gt;KV needed vs v5e pool&lt;/th&gt;
&lt;th&gt;v5e @ $0.60&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v6e @ $1.35&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;v6e vs v5e&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;$1.352&lt;/td&gt;
&lt;td&gt;$1.848&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.37× dearer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;td&gt;$0.226&lt;/td&gt;
&lt;td&gt;$0.314&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.39× dearer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;$0.186&lt;/td&gt;
&lt;td&gt;$0.249&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.34× dearer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;$0.284&lt;/td&gt;
&lt;td&gt;$0.276&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.97× — break-even&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;$0.542&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.494&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.91× — cheaper&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;161%&lt;/td&gt;
&lt;td&gt;$0.999&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.867&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.87× — cheaper&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;321%&lt;/td&gt;
&lt;td&gt;$1.000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.841&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.84× — cheaper&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;166%&lt;/td&gt;
&lt;td&gt;$0.530&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.431&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.81× — cheaper&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1.545&lt;/td&gt;
&lt;td&gt;v6e only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One chip 24/7:&lt;/strong&gt; $438/month on v5e flex-start, &lt;strong&gt;$986/month on v6e flex-start&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing
&lt;/h3&gt;

&lt;p&gt;The rule follows from the table, and it is a memory question rather than a throughput one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Determine whether the steady-state working set — &lt;code&gt;clients × context&lt;/code&gt; — exceeds ~270,000 tokens,&lt;br&gt;
roughly 84% of a v5e's pool. Below that, v5e is 26–29% cheaper per token. Above it, v6e is cheaper,&lt;br&gt;
but by 3% at the boundary and never by more than 19%&lt;/strong&gt;, because v5e can no longer hold the job and&lt;br&gt;
starts recomputing it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things follow that the memory ratio alone would not predict. &lt;strong&gt;The advantage saturates:&lt;/strong&gt; once past the boundary, v6e settles at 0.81–0.87× and does not keep improving with working-set size — 16,000 × 64 runs at 321% of a v5e pool and is still only 16% cheaper. And &lt;strong&gt;break-even is not a comfortable margin.&lt;/strong&gt; At 84% of pool the two chips are within 3% of each other, which is inside the run-to-run spread of the band those cells sit in.&lt;/p&gt;

&lt;p&gt;This is not "v6e for long context". The &lt;code&gt;4096 × 64&lt;/code&gt; cell is only 4K of context and reaches the boundary anyway, because 64 clients × 4,096 tokens is 270,336 KV tokens. &lt;strong&gt;Concurrency crosses the line as readily as context does.&lt;/strong&gt; The threshold is a product, which is why the single-stream cell is v6e's worst showing at 1.37× dearer.&lt;/p&gt;

&lt;p&gt;One effect dominates both columns: moving from a single stream to the best-measured concurrent cell is a &lt;strong&gt;7.4× cost reduction per token on v6e&lt;/strong&gt; ($1.848/1M at 128 ctx × 1 client → $0.249/1M at 1,024 ctx × 16). That is larger than the 2.25× between the two chips and larger than any other effect here. &lt;strong&gt;Tune concurrency before shopping for chips.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing above 157% of pool, and nothing that forces preemption.&lt;/strong&gt; Every cell was admission-controlled; the eviction regime — the condition that would actually produce a cliff — is untested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The knee sweep varied concurrency at fixed context only.&lt;/strong&gt; A context sweep at fixed concurrency loads the 3 full-attention layers differently and is not covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 1.65× control figure is unexplained&lt;/strong&gt;, sitting below the bandwidth ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single run per cell, on two independent nodes.&lt;/strong&gt; Every cell in the results table was measured twice, on separate v6e-1 nodes in different zones. Seven of ten agreed to within 6.3%; the three in the 4,096–8,192 band diverged 12–19% and are reported at the clean-seed value. No within-node variance figure is established, so a difference under ~6% between any two cells here is not a result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-boot and compile timings are unmeasured on v6e.&lt;/strong&gt; The v5e figures — 857 s cold, 685 s of it compilation, 497 s with the compile cache mounted — describe a mechanism that carries, not timings that do. v6e compiles its own kernels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--gpu-memory-utilization&lt;/code&gt; has no established v6e ceiling.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qwix int8 weights, &lt;code&gt;max_model_len 65536&lt;/code&gt;, &lt;code&gt;max-num-batched-tokens 8192&lt;/code&gt;, &lt;code&gt;VLLM_TPU_BUCKET_PADDING_GAP=128&lt;/code&gt;, and n-gram speculative decoding&lt;/strong&gt; are untried on this chip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upstream flags E2B's correctness tests as failing&lt;/strong&gt; — tpu-inference's support table marks &lt;code&gt;gemma-4-E2B-it&lt;/code&gt; ✅ unit / ❌ correctness / ❓ performance, while the 26B and 31B pass all three. Quality probes on v5e were clean (8/9 byte-identical outputs, 3/3 needle retrievals at 2K/8K/14K).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Self-hosting a lite agent backend on one TPU: Gemma 4 E2B + vLLM on a v5e-1"&lt;/strong&gt; — the companion piece, and the source of every v5e-1 figure quoted here: the 321,376-token KV pool, the 8.02 ms single-stream TPOT, the &lt;code&gt;--gpu-memory-utilization 0.95&lt;/code&gt; failure at 691 s, the 1.000× fp8 KV result, and the per-cell throughput used in the ratio and cost tables. Same model, same engine family, same &lt;code&gt;OUTPUT_LEN&lt;/code&gt;, one chip generation down.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://xbill999.medium.com/debugging-deployments-with-gemma-4b-tpu-v6e-1-mcp-and-antigravity-cli-c9846231237a" rel="noopener noreferrer"&gt;Debugging deployments with Gemma 4 4B, TPU v6e-1, MCP and Antigravity CLI&lt;/a&gt; — the earlier v6e-1 write-up, covering the MCP tooling and the deploy path rather than the serving numbers.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.cloud.google.com/tpu/docs/v6e" rel="noopener noreferrer"&gt;Cloud TPU v6e&lt;/a&gt; and &lt;a href="https://docs.cloud.google.com/tpu/docs/v5e" rel="noopener noreferrer"&gt;Cloud TPU v5e&lt;/a&gt; documentation — the source of both spec columns, and of the GiBps/GBps units mismatch: the v5e page quotes HBM bandwidth in GiBps while the v6e page uses GBps.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/dws" rel="noopener noreferrer"&gt;Dynamic Workload Scheduler pricing&lt;/a&gt; — flex-start is billed under DWS, which is why the SKU is named "DWS Defined Duration V6e" rather than anything containing "flex".&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.cloud.google.com/tpu/docs/system-architecture-tpu-vm" rel="noopener noreferrer"&gt;TPU system architecture&lt;/a&gt; — the 256×256-versus-128×128 MXU dimensions behind the unresolved MXU-count arithmetic in §2.1.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Appendix: operational traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A published price is not an offer of capacity, or even of the provisioning model.&lt;/strong&gt; Three independent gates; quota is the weakest signal and the one most often checked first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google's regions-and-zones page undercounts v6e zones&lt;/strong&gt; — 8 documented against 18 the API accepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WAITING_FOR_RESOURCES&lt;/code&gt; is not a failure.&lt;/strong&gt; Do not record it as one and do not tear the request down; flex-start capacity can take two hours to return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v6e quota ids drop the &lt;code&gt;Litepod&lt;/code&gt; that v5e's carry&lt;/strong&gt;, and a stale id matches no rows rather than erroring — indistinguishable from "no quota anywhere".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/v1/completions&lt;/code&gt; returns an empty string on &lt;code&gt;-it&lt;/code&gt; models.&lt;/strong&gt; Use &lt;code&gt;/v1/chat/completions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoints are not stable.&lt;/strong&gt; The external IP changes every time the node is recreated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google quotes v5e bandwidth in GiBps and v6e in GBps.&lt;/strong&gt; Normalise before dividing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;31.24 GiB&lt;/code&gt; and &lt;code&gt;33.55 GB&lt;/code&gt; are the same number.&lt;/strong&gt; XLA prints GiB; &lt;code&gt;memory_analysis()&lt;/code&gt; returns bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XLA compares temporaries alone against the whole chip&lt;/strong&gt; and does not subtract resident weights. &lt;code&gt;available HBM (31.24G)&lt;/code&gt; in an error message is not headroom.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tpu</category>
      <category>vllm</category>
      <category>llm</category>
      <category>gcp</category>
    </item>
    <item>
      <title>Three Clouds, Three Native Agents</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Mon, 10 Aug 2026 15:04:09 +0000</pubDate>
      <link>https://dev.to/gde/three-clouds-three-native-agents-3egf</link>
      <guid>https://dev.to/gde/three-clouds-three-native-agents-3egf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd0vye2o57uqbmx18k6n.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd0vye2o57uqbmx18k6n.jpg" alt="A Cloud Run coordinator calling an ADK agent on Cloud Run, a Strands agent on Bedrock AgentCore, and an Agent Framework agent on Container Apps, over A2A v1.0 with no stored secrets" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is this project trying to do?
&lt;/h2&gt;

&lt;p&gt;Three AI agents, each built with a different vendor's framework, each running on&lt;br&gt;
that vendor's own hosting, all answering the same question at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; — an ADK agent on Cloud Run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS&lt;/strong&gt; — a Strands agent on Bedrock AgentCore Runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure&lt;/strong&gt; — an Agent Framework agent on Container Apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One coordinator calls all three over &lt;strong&gt;A2A v1.0&lt;/strong&gt; and takes the median of their&lt;br&gt;
answers. And there is &lt;strong&gt;no long-lived credential stored anywhere in the running&lt;br&gt;
system&lt;/strong&gt; — every call is authenticated with a token minted at the moment it is&lt;br&gt;
needed.&lt;/p&gt;

&lt;p&gt;Everything is here:&lt;br&gt;
&lt;a href="https://github.com/xbill9/multicloud-adk-a2a-currency" rel="noopener noreferrer"&gt;github.com/xbill9/multicloud-adk-a2a-currency&lt;/a&gt;.&lt;br&gt;
You can run the whole mesh on a laptop in about a minute; instructions are below.&lt;/p&gt;

&lt;p&gt;The surprise wasn't the protocol. A2A worked. The surprise was that almost every&lt;br&gt;
decision that mattered was made &lt;em&gt;before&lt;/em&gt; a single A2A call happened.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why bother? Just use a key
&lt;/h2&gt;

&lt;p&gt;You have an agent on one cloud. Someone asks you to have it call an agent on&lt;br&gt;
another.&lt;/p&gt;

&lt;p&gt;The reflex is to create a service account key, drop it in a secret manager, and&lt;br&gt;
move on. That works. It also means you now own a credential forever — rotating&lt;br&gt;
it, scoping it, auditing it, and eventually explaining to somebody why&lt;br&gt;
production contains a static key.&lt;/p&gt;

&lt;p&gt;There is another way, and the interesting part is that it isn't harder. It is&lt;br&gt;
just decided earlier.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one decision that sets everything else
&lt;/h2&gt;

&lt;p&gt;Here is the asymmetry the whole design falls out of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every agent you want to call can consume an external token.&lt;/strong&gt; AWS IAM has OIDC&lt;br&gt;
identity providers. Entra has Federated Identity Credentials. AgentCore accepts a&lt;br&gt;
&lt;code&gt;CUSTOM_JWT&lt;/code&gt;. All three will trust a token minted somewhere else, provided you&lt;br&gt;
set the trust up correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But only some runtimes can mint one.&lt;/strong&gt; A runtime that can produce a workload&lt;br&gt;
OIDC token — for an audience &lt;em&gt;you&lt;/em&gt; choose — can federate outward to any of them.&lt;br&gt;
A runtime that cannot is back to storing a credential.&lt;/p&gt;

&lt;p&gt;So "where does my coordinator run?" is really "how many secrets will this system&lt;br&gt;
have?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coordinator host&lt;/th&gt;
&lt;th&gt;Legs it makes&lt;/th&gt;
&lt;th&gt;Long-lived secrets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GCP→AWS, GCP→Azure, GCP→GCP&lt;/td&gt;
&lt;td&gt;potentially &lt;strong&gt;zero&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AgentCore&lt;/td&gt;
&lt;td&gt;AWS→Azure, AWS→GCP&lt;/td&gt;
&lt;td&gt;at least one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foundry&lt;/td&gt;
&lt;td&gt;Azure→AWS, Azure→GCP&lt;/td&gt;
&lt;td&gt;one or two, both unproven&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloud Run wins here because its metadata server hands you an ID token for any&lt;br&gt;
audience you name, which is exactly what the other two clouds' trust policies&lt;br&gt;
want to see. Whether AgentCore can do the same is unconfirmed — I did not test&lt;br&gt;
it. So "zero secrets" is a property of &lt;em&gt;this&lt;/em&gt; topology, not a law about&lt;br&gt;
cross-cloud agents.&lt;/p&gt;

&lt;p&gt;Two things that choice costs you, worth saying out loud:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One leg stops being cross-cloud.&lt;/strong&gt; The coordinator runs on Cloud Run, so the&lt;br&gt;
GCP leg is Google calling Google. Two vendor boundaries get crossed, not three.&lt;br&gt;
That belongs in the results, not in a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot run it locally.&lt;/strong&gt; A user credential cannot mint an&lt;br&gt;
arbitrary-audience ID token at all — &lt;code&gt;gcloud auth print-identity-token&lt;br&gt;
--audiences=...&lt;/code&gt; refuses outright, telling you it requires a service account.&lt;br&gt;
There is no laptop version of this path. Once you choose federation, the only&lt;br&gt;
place the system works is the place it is deployed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three legs, three mechanisms, one seam
&lt;/h2&gt;

&lt;p&gt;The legs do not look alike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GCP → GCP&lt;/strong&gt; — an ID token whose audience is the target service's URL, plus
&lt;code&gt;roles/run.invoker&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP → AWS&lt;/strong&gt; — mint that token, hand it to STS &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;,
get temporary credentials back, sign the request with SigV4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP → Azure&lt;/strong&gt; — mint that token, present it to Entra as a client assertion
against a Federated Identity Credential, get an access token back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two bearer tokens and a request signature. Different shapes entirely.&lt;/p&gt;

&lt;p&gt;The move that made the rest tractable was putting all three behind one interface:&lt;br&gt;
&lt;code&gt;httpx.Auth&lt;/code&gt;. To httpx, a bearer header and a signature over the request body are&lt;br&gt;
the same kind of object. All three vendor SDKs accept an &lt;code&gt;httpx.AsyncClient&lt;/code&gt;. So&lt;br&gt;
the credential attaches once, and everything through that client carries it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;credentials_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# an httpx.Auth, or None
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build that seam &lt;strong&gt;before&lt;/strong&gt; your second cloud, not after your third. Get one leg&lt;br&gt;
working with inline code and promise to generalise later, and you end up with&lt;br&gt;
three error-handling styles and three places a token gets cached.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth noticing:&lt;/strong&gt; an agent's card lives at &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;,&lt;br&gt;
and it sits behind the same authorization as the agent itself. Attach your&lt;br&gt;
credential to the &lt;em&gt;request&lt;/em&gt; instead of the &lt;em&gt;client&lt;/em&gt; and discovery 403s while&lt;br&gt;
the actual call would have worked. You get a protocol error pointing nowhere&lt;br&gt;
near auth. Attaching to the client makes that impossible by construction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Five traps that look exactly like working configuration
&lt;/h2&gt;

&lt;p&gt;None of these are typos. Each is something you can get wrong while being careful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audience is not authorization.&lt;/strong&gt; The &lt;em&gt;caller&lt;/em&gt; picks the audience. So a trust&lt;br&gt;
policy checking only audience proves that &lt;em&gt;somebody&lt;/em&gt; in that IdP minted a token —&lt;br&gt;
not that &lt;em&gt;your&lt;/em&gt; identity did. Pin the subject too, and pin it to the immutable&lt;br&gt;
numeric ID rather than the email, because emails can be released and re-bound to&lt;br&gt;
someone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS and Azure invert the same step.&lt;/strong&gt; AWS federates with &lt;code&gt;accounts.google.com&lt;/code&gt;&lt;br&gt;
natively — create an explicit IAM OIDC provider for it and you &lt;em&gt;break&lt;/em&gt; it with&lt;br&gt;
&lt;code&gt;InvalidIdentityToken&lt;/code&gt;. Entra requires you to create the credential explicitly.&lt;br&gt;
Same conceptual task, opposite prerequisites, and neither error tells you which&lt;br&gt;
rule you are on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The IAM condition keys do not hold what their names say.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;accounts.google.com:oaud&lt;/code&gt; is the token's &lt;code&gt;aud&lt;/code&gt;. &lt;code&gt;accounts.google.com:aud&lt;/code&gt; is its&lt;br&gt;
&lt;code&gt;azp&lt;/code&gt;, which is a number. Put an audience string in &lt;code&gt;:aud&lt;/code&gt; and you have written a&lt;br&gt;
condition that can never match. The denial will not mention it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask for the whole token.&lt;/strong&gt; The GCP metadata mint takes &lt;code&gt;format=full&lt;/code&gt;. Without&lt;br&gt;
it, Google trims claims — including &lt;code&gt;email&lt;/code&gt; — and any trust condition reading&lt;br&gt;
that claim silently stops matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two error codes are worth more than a day of logging.&lt;/strong&gt; From STS,&lt;br&gt;
&lt;code&gt;InvalidIdentityToken&lt;/code&gt; means the token did not validate at all, which is a&lt;br&gt;
provider-setup problem. &lt;code&gt;AccessDenied&lt;/code&gt; means it validated fine and your&lt;br&gt;
conditions did not match, which is a policy problem. Different afternoons.&lt;/p&gt;

&lt;p&gt;Which leads to the one habit I would carry to any project like this: &lt;strong&gt;log the&lt;br&gt;
raw provider response at every auth boundary.&lt;/strong&gt; In an agent system an error comes&lt;br&gt;
back as a tool result, and a model in the middle will cheerfully paraphrase&lt;br&gt;
&lt;code&gt;AccessDenied: condition accounts.google.com:sub did not match&lt;/code&gt; into "there was&lt;br&gt;
an issue with the credentials." A raised message is not an observable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Start local. Three agents on loopback, no cloud account, about a minute:&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/xbill9/multicloud-adk-a2a-currency
&lt;span class="nb"&gt;cd &lt;/span&gt;multicloud-adk-a2a-currency

uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="s2"&gt;"a2a-sdk[http-server]"&lt;/span&gt; google-adk &lt;span class="se"&gt;\&lt;/span&gt;
  agent-framework-a2a agent-framework-core &lt;span class="se"&gt;\&lt;/span&gt;
  pydantic httpx uvicorn pytest pytest-asyncio
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring up the three agents and ask them a question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./infra/run_mesh.sh start          &lt;span class="c"&gt;# :10001 :10002 :10003&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; coordinator.cli 100 USD EUR JPY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three vendors' agent stacks answering together:&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;participants&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp, aws, azure&lt;/span&gt;

&lt;span class="s"&gt;100 USD = 92 EUR @ 0.92 [3/3 clouds, agreed]&lt;/span&gt;
    &lt;span class="s"&gt;gcp                  92 (164ms)&lt;/span&gt;
    &lt;span class="s"&gt;aws                  92 (25ms)&lt;/span&gt;
    &lt;span class="s"&gt;azure                92 (12ms)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo is the more interesting run, because it shows what happens when a&lt;br&gt;
participant is &lt;em&gt;wrong&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./infra/demo.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four acts: three clouds answering, the 3×3 interop matrix, a cloud going&lt;br&gt;
offline, and a cloud lying. The last two are the point — anything can show three&lt;br&gt;
green ticks.&lt;/p&gt;

&lt;p&gt;Deploying for real is one script per cloud, then one command to wire them&lt;br&gt;
together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./infra/deploy_aws.sh   deploy     &lt;span class="c"&gt;# AgentCore Runtime + federated role&lt;/span&gt;
./infra/deploy_azure.sh deploy     &lt;span class="c"&gt;# Container App&lt;/span&gt;
./infra/deploy_azure.sh fic        &lt;span class="c"&gt;# Entra app registration + federated credential&lt;/span&gt;
./infra/deploy_azure.sh auth       &lt;span class="c"&gt;# make the ingress actually demand it&lt;/span&gt;

./infra/deploy_gcp.sh deploy       &lt;span class="c"&gt;# ADK service + coordinator job&lt;/span&gt;
./infra/deploy_gcp.sh wire         &lt;span class="c"&gt;# fold the AWS and Azure legs in&lt;/span&gt;
./infra/deploy_gcp.sh run          &lt;span class="c"&gt;# three-cloud consensus, from the cloud&lt;/span&gt;
./infra/deploy_gcp.sh verify       &lt;span class="c"&gt;# the negative controls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Run &lt;code&gt;verify&lt;/code&gt; twice.&lt;/strong&gt; It is the part that decides whether any of the auth&lt;br&gt;
claims mean anything, for a reason covered below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deployment decisions that aged well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Put deployment in the repo as verbs, not in a runbook.&lt;/strong&gt; &lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;wire&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;verify&lt;/code&gt;. Each cloud's identifiers live in exactly one place — the script that&lt;br&gt;
created them — and the other scripts read them back rather than keeping copies.&lt;/p&gt;

&lt;p&gt;I can tell you precisely what that buys, because I tore the entire mesh down and&lt;br&gt;
rebuilt it from nothing to check.&lt;/p&gt;

&lt;p&gt;The AWS runtime came back with a &lt;strong&gt;different ARN&lt;/strong&gt;, and its invocation URL&lt;br&gt;
contains that ARN. The Entra app registration came back with a &lt;strong&gt;different client&lt;br&gt;
ID&lt;/strong&gt;. The Container App came back on a &lt;strong&gt;different FQDN&lt;/strong&gt;. Nothing was edited by&lt;br&gt;
hand. &lt;code&gt;wire&lt;/code&gt; read all three back out and the mesh returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 USD = 92 EUR @ 0.92 [3/3 clouds, agreed]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any copy of any of those identifiers stored anywhere else would have been stale&lt;br&gt;
the moment it was written down.&lt;/p&gt;

&lt;p&gt;Then the whole verification pass ran again against infrastructure that had not&lt;br&gt;
existed an hour earlier: three consensus runs at &lt;code&gt;3/3 clouds, agreed&lt;/code&gt;, and all&lt;br&gt;
eight auth probes — each leg answering with its credential, each leg denied&lt;br&gt;
without it, an unauthenticated request rejected, and a right-identity&lt;br&gt;
wrong-audience request rejected. Every number in this article comes from that&lt;br&gt;
rebuilt mesh.&lt;/p&gt;

&lt;p&gt;That teardown also found two bugs that no amount of redeploying would have,&lt;br&gt;
because they live on code paths you can only reach from nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A retry wrapper I had added to the AWS script made "no runtime exists" return
an error instead of the string &lt;code&gt;None&lt;/code&gt;. Under &lt;code&gt;set -e&lt;/code&gt;, a &lt;em&gt;first&lt;/em&gt; deploy died
silently before ever creating the runtime. Every deploy since I wrote it had
taken the update branch, so nothing ran the broken path.&lt;/li&gt;
&lt;li&gt;Azure &lt;strong&gt;soft-deletes&lt;/strong&gt; Cognitive Services accounts. Deleting the resource group
does not purge them, so recreating by the same name fails with
&lt;code&gt;FlagMustBeSetForRestore&lt;/code&gt; — an error that never mentions deletion. &lt;code&gt;destroy&lt;/code&gt;
followed by &lt;code&gt;deploy&lt;/code&gt; could not rebuild the Foundry account.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you take one operational thing from this article:&lt;/strong&gt; rebuild from nothing&lt;br&gt;
at least once before you tell anyone it is reproducible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scale to zero, and label what it costs.&lt;/strong&gt; Everything here idles at zero&lt;br&gt;
replicas. Paying for idle capacity on three clouds to make a latency table look&lt;br&gt;
tidier is paying to mislead. But it means the first call into a leg pays a cold&lt;br&gt;
start — a cold Azure leg measured &lt;strong&gt;27.8 seconds&lt;/strong&gt; against &lt;strong&gt;0.5 seconds&lt;/strong&gt; warm.&lt;br&gt;
Mix those two regimes in one table and every conclusion drawn from it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffolding worth stealing
&lt;/h2&gt;

&lt;p&gt;Four structures did most of the work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Structure&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One credential seam (&lt;code&gt;httpx.Auth&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;callers never know which of three mechanisms they are using&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One participant interface (&lt;code&gt;convert()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;a cloud is an implementation, not a branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An instrument, not a demo&lt;/td&gt;
&lt;td&gt;every failure typed by layer, not just red&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controls scoped to one leg&lt;/td&gt;
&lt;td&gt;a degrading system cannot hide a denial from you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one is the one I would most want you to copy, because getting it wrong&lt;br&gt;
is invisible.&lt;/p&gt;

&lt;p&gt;The mesh takes a median across three clouds and degrades on purpose. Lose a&lt;br&gt;
cloud, the other two still reach quorum, and the run exits &lt;strong&gt;0&lt;/strong&gt;. Now try testing&lt;br&gt;
your auth by removing one leg's credential from a three-cloud run. It still exits&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;That reads as "no denial happened." What actually happened is "the denial was
absorbed."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So every leg gets probed alone. Eight probes: each leg answering with its&lt;br&gt;
credential, each leg denied without it, an unauthenticated request rejected, and&lt;br&gt;
a right-identity-wrong-audience request rejected. Only then does an exit code&lt;br&gt;
mean anything.&lt;/p&gt;

&lt;p&gt;The general form: &lt;strong&gt;any system with graceful degradation needs its controls&lt;br&gt;
scoped to a single component, or the degradation hides exactly the failure you&lt;br&gt;
are testing for.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Warm runs of the three-cloud consensus, after the rebuild:&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;GCP (in-cloud)&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;th&gt;Azure&lt;/th&gt;
&lt;th&gt;elapsed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;range&lt;/td&gt;
&lt;td&gt;836–948ms&lt;/td&gt;
&lt;td&gt;1027–1109ms&lt;/td&gt;
&lt;td&gt;468–512ms&lt;/td&gt;
&lt;td&gt;1711–1854ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Elapsed lands roughly a second above the &lt;em&gt;slowest single leg&lt;/em&gt;, and far below the&lt;br&gt;
sum of all three. The legs are issued concurrently, so the sum was never the&lt;br&gt;
right model — but neither is the slowest leg on its own. That extra second is the&lt;br&gt;
coordinator's own fixed cost: container start, three agent-card fetches, three&lt;br&gt;
credential mints.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth noticing:&lt;/strong&gt; an earlier version of this claim quoted the slowest leg&lt;br&gt;
alone and was &lt;strong&gt;wrong by 85%&lt;/strong&gt; on the fastest run. That error only became&lt;br&gt;
visible once there was more than one sample.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The federation itself is cheap. Token mints and exchanges are a small slice of&lt;br&gt;
that fixed second. If the mesh feels slow, it is a cold start or a model — not&lt;br&gt;
the identity work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;One deployment, one account, one region pair, one person, over a few days. These&lt;br&gt;
are existence proofs: a thing worked, in a configuration. They are not&lt;br&gt;
measurements of a population.&lt;/p&gt;

&lt;p&gt;It is keyless in operation, not in bootstrap. Creating trust policies, app&lt;br&gt;
registrations and federated credentials used ordinary operator credentials, as&lt;br&gt;
provisioning always does.&lt;/p&gt;

&lt;p&gt;And that claim needed checking, which is the honest part. The three A2A legs were&lt;br&gt;
always keyless — but the Azure app pulled its container image using the&lt;br&gt;
registry's admin password, stored as a secret in its own configuration. Not on&lt;br&gt;
any agent-to-agent path, and still enough to make "no stored secrets" false as&lt;br&gt;
written. Container Apps supports pulling by managed identity, so the fix was a&lt;br&gt;
role grant and deleting the secret. An audit of all three deployments now shows&lt;br&gt;
no stored credential in any of them.&lt;/p&gt;

&lt;p&gt;The dull general point: &lt;strong&gt;image pull is part of your deployed system.&lt;/strong&gt; A claim&lt;br&gt;
about secrets has to cover all of it, not just the interesting part.&lt;/p&gt;

&lt;p&gt;Token expiry and refresh are implemented and tested against a frozen clock, but&lt;br&gt;
no token has ever expired in production — every run is a job that lives a few&lt;br&gt;
seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are starting one of these
&lt;/h2&gt;

&lt;p&gt;Decide where the coordinator runs before anything else; it sets the secret count&lt;br&gt;
for the entire system. Build the credential seam before the second cloud. Attach&lt;br&gt;
auth to the client, not the request, so discovery is covered. Log the provider's&lt;br&gt;
own words at every boundary, because you will spend more time reading auth&lt;br&gt;
failures than writing auth code. Scope your controls to one component, because a&lt;br&gt;
system built to survive failure will happily hide one from you.&lt;/p&gt;

&lt;p&gt;And rebuild it from nothing once, before you claim it is reproducible.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/xbill9/multicloud-adk-a2a-currency" rel="noopener noreferrer"&gt;github.com/xbill9/multicloud-adk-a2a-currency&lt;/a&gt;&lt;br&gt;
— the three agents, the coordinator, the interop matrix, the deploy scripts, and&lt;br&gt;
the findings write-ups in &lt;code&gt;docs/&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
