<?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: xbill</title>
    <description>The latest articles on DEV Community by xbill (@xbill).</description>
    <link>https://dev.to/xbill</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3490099%2Fc6a975d0-cd94-485d-82b1-14ed5b344fcf.jpg</url>
      <title>DEV Community: xbill</title>
      <link>https://dev.to/xbill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xbill"/>
    <language>en</language>
    <item>
      <title>Build an MCP server in Rust with rmcp: a walk-through 🦀</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:32:56 +0000</pubDate>
      <link>https://dev.to/gde/build-an-mcp-server-in-rust-with-rmcp-a-walk-through-4cif</link>
      <guid>https://dev.to/gde/build-an-mcp-server-in-rust-with-rmcp-a-walk-through-4cif</guid>
      <description>&lt;p&gt;This tutorial walks through building an &lt;strong&gt;MCP server in Rust&lt;/strong&gt; with&lt;br&gt;
&lt;a href="https://crates.io/crates/rmcp" rel="noopener noreferrer"&gt;&lt;code&gt;rmcp&lt;/code&gt;&lt;/a&gt;, the official Model Context Protocol Rust SDK.&lt;/p&gt;

&lt;p&gt;The example is a real one: a devops agent that manages &lt;strong&gt;AWS EC2 G5g&lt;/strong&gt; instances — Graviton2&lt;br&gt;
boxes with NVIDIA T4G GPUs — serving Gemma 4 under vLLM. It launches instances, drives them&lt;br&gt;
over SSM, and health-checks the model. There's an existing Python version, so at the end we&lt;br&gt;
can put the two side by side.&lt;/p&gt;

&lt;p&gt;Follow along and you'll have a working, registerable MCP server. 🦀&lt;/p&gt;


&lt;h4&gt;
  
  
  Why Rust for this?
&lt;/h4&gt;

&lt;p&gt;Worth answering properly, because the weak version of the argument is easy to make and easy to&lt;br&gt;
demolish — and the real one is better anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with what it isn't: these tools are I/O bound.&lt;/strong&gt; Every one is an AWS API call —&lt;br&gt;
&lt;code&gt;describe_instances&lt;/code&gt;, &lt;code&gt;send_command&lt;/code&gt;, polling SSM — so 100–500 ms of network per call. The&lt;br&gt;
caller's language contributes nothing measurable there. Anyone selling you a Rust rewrite on&lt;br&gt;
raw speed for this workload is selling something.&lt;/p&gt;

&lt;p&gt;Three claims that don't hold, so nobody has to make them in the comments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Why it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"462 ms startup is slow"&lt;/td&gt;
&lt;td&gt;stdio servers spawn &lt;strong&gt;once per session&lt;/strong&gt;, not per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Rust is faster"&lt;/td&gt;
&lt;td&gt;the work is network round-trips to AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"smaller supply chain"&lt;/td&gt;
&lt;td&gt;241 crates vs 34 Python packages — it's &lt;em&gt;worse&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What actually justifies it, for this codebase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's a fleet, not a server.&lt;/strong&gt; This monorepo has &lt;strong&gt;16 rigs&lt;/strong&gt;, each with its own MCP&lt;br&gt;
server. That changes the units:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;All loaded together&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resident memory&lt;/td&gt;
&lt;td&gt;16 × 83 MB ≈ &lt;strong&gt;1.33 GB&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;16 × 12 MB ≈ &lt;strong&gt;192 MB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session startup&lt;/td&gt;
&lt;td&gt;16 × 462 ms ≈ &lt;strong&gt;7.4 s&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;16 × 2.5 ms ≈ &lt;strong&gt;40 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A gigabyte of resident Python to expose sixteen tool lists is a real cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No shared interpreter.&lt;/strong&gt; These rigs install system-wide — no virtualenvs, by policy — so&lt;br&gt;
all sixteen share one Python. Sixteen servers with independently drifting &lt;code&gt;boto3&lt;/code&gt; and &lt;code&gt;mcp&lt;/code&gt;&lt;br&gt;
pins in one interpreter is a standing conflict risk. A static binary has no such coupling;&lt;br&gt;
each rig pins whatever it likes in its own &lt;code&gt;Cargo.lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The schema can't drift from the code.&lt;/strong&gt; More on this at Step 3, but it's the one that&lt;br&gt;
survives longest: &lt;code&gt;schemars&lt;/code&gt; generates the tool schema from the same struct the handler&lt;br&gt;
destructures.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;distribution and correctness, not speed.&lt;/strong&gt; ✅ If you have one MCP server and it works,&lt;br&gt;
this is not a reason to rewrite it.&lt;/p&gt;


&lt;h4&gt;
  
  
  How does this all fit together?
&lt;/h4&gt;

&lt;p&gt;Two halves. The agent and the MCP server run on your machine; the GPU box is remote, and it&lt;br&gt;
has &lt;strong&gt;no inbound SSH&lt;/strong&gt; — everything goes through the AWS APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   YOUR MACHINE                                     AWS  us-east-1
┌──────────────────────────────┐       ┌───────────────────────────────────────┐
│                              │       │                                       │
│  Claude Code / IDE           │       │  ┌─ EC2 g5g.4xlarge ───────────────┐  │
│         |                    │       │  │  Graviton2 (aarch64)            │  │
│         | MCP · JSON-RPC 2.0 │       │  │  + NVIDIA T4G (SM 7.5)          │  │
│         | over stdio         │       │  │                                 │  │
│         v                    │  EC2  │  │  [PY] vLLM + [RUST] vllm-rs     │  │
│  ┌────────────────────────┐  │  API  │  │  listening on :8000             │  │
│  │ [RUST]                 │──┼──────&amp;gt;│  │                                 │  │
│  │ gpu-vllm-g5g-2b        │  │       │  │  Gemma 4 E2B                    │  │
│  │                        │  │  SSM  │  └─────────────────────────────────┘  │
│  │ rmcp 3.1.2             │──┼──────&amp;gt;│           ^                           │
│  │ tokio · schemars       │  │  Run  │           |  no inbound SSH,          │
│  │ aws-sdk-ec2 / -ssm     │  │  Cmd  │           |  no key pair,             │
│  │ 1 binary · 2.5 ms      │  │       │           |  no port 22 rule          │
│  └────────────────────────┘  │       │                                       │
│        9 tools               │       │  IAM instance profile carries         │
│  list / start / stop /       │       │  AmazonSSMManagedInstanceCore         │
│  terminate / endpoint /      │       │                                       │
│  run_remote / health ...     │       │                                       │
└──────────────────────────────┘       └───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent never talks to the GPU box directly. It calls a tool; the tool calls &lt;strong&gt;EC2&lt;/strong&gt; to&lt;br&gt;
manage the instance's lifecycle, or &lt;strong&gt;SSM Run Command&lt;/strong&gt; to execute something on it. That's&lt;br&gt;
what lets the box run with no inbound rules at all — which is the main reason this is worth&lt;br&gt;
building as a server rather than a pile of shell scripts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[RUST]&lt;/code&gt; on the right-hand side is vLLM's own Rust frontend — the other article in this&lt;br&gt;
series. This one is the &lt;code&gt;[RUST]&lt;/code&gt; on the left: the Rust that drives the box.&lt;/p&gt;


&lt;h4&gt;
  
  
  What is MCP, in one paragraph?
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Model Context Protocol&lt;/strong&gt; is how an AI agent discovers and calls your tools. Your server&lt;br&gt;
advertises a list of tools with JSON Schemas; the client (Claude Code, an IDE, whatever)&lt;br&gt;
calls them over JSON-RPC 2.0. Transport is usually &lt;strong&gt;stdio&lt;/strong&gt; — the client spawns your binary&lt;br&gt;
and talks over stdin/stdout.&lt;/p&gt;

&lt;p&gt;That last detail matters for the Rust pitch: if the client spawns your process on every&lt;br&gt;
session, &lt;strong&gt;process startup is a user-visible cost&lt;/strong&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 1 — Scaffold
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo new &lt;span class="nt"&gt;--bin&lt;/span&gt; rust-mcp &lt;span class="nt"&gt;--name&lt;/span&gt; gpu-vllm-g5g-2b-mcp
&lt;span class="nb"&gt;cd &lt;/span&gt;rust-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the dependencies. &lt;strong&gt;Feature flags are the thing to get right here&lt;/strong&gt; — &lt;code&gt;cargo add rmcp&lt;/code&gt;&lt;br&gt;
on its own compiles fine and gives you almost nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add rmcp &lt;span class="nt"&gt;--features&lt;/span&gt; server,macros,transport-io
&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;Feature&lt;/th&gt;
&lt;th&gt;What it brings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;ServerHandler&lt;/code&gt; trait and router types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macros&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;#[tool]&lt;/code&gt;, &lt;code&gt;#[tool_router]&lt;/code&gt;, &lt;code&gt;#[tool_handler]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transport-io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stdio transport&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The crate also ships &lt;code&gt;client&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;elicitation&lt;/code&gt;, &lt;code&gt;transport-streamable-http-server&lt;/code&gt; and&lt;br&gt;
more, all off by default. Add them when you need them.&lt;/p&gt;

&lt;p&gt;Then the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; rt-multi-thread,macros,process,time
cargo add serde serde_json anyhow schemars
cargo add aws-config aws-sdk-ec2 aws-sdk-ssm aws-sdk-secretsmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resulting &lt;code&gt;Cargo.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;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpu-vllm-g5g-2b-mcp"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;edition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2024"&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;rmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"macros"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transport-io"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.53.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rt-multi-thread"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"macros"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;aws-config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.10.1"&lt;/span&gt;
&lt;span class="py"&gt;aws-sdk-ec2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.246.0"&lt;/span&gt;
&lt;span class="py"&gt;aws-sdk-ssm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.118.0"&lt;/span&gt;
&lt;span class="py"&gt;serde&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.229"&lt;/span&gt;
&lt;span class="py"&gt;serde_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.151"&lt;/span&gt;
&lt;span class="py"&gt;schemars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.2.2"&lt;/span&gt;
&lt;span class="py"&gt;anyhow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.104"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🔎 Tip: where the canonical examples live
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;rmcp&lt;/code&gt; moves fast, and rendered docs lag. The &lt;strong&gt;vendored tests on your own disk&lt;/strong&gt; are&lt;br&gt;
compiled against the exact version you resolved:&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;ls&lt;/span&gt; ~/.cargo/registry/src/&lt;span class="k"&gt;*&lt;/span&gt;/rmcp-3.1.2/tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tests/test_tool_macros.rs&lt;/code&gt; is a complete, working server in about 60 lines. When an API&lt;br&gt;
question comes up, that file answers it faster and more reliably than anything else. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 2 — The server struct
&lt;/h4&gt;

&lt;p&gt;An rmcp server is a struct that owns a &lt;code&gt;ToolRouter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;rmcp&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServiceExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;server&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nn"&gt;model&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContentBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerCapabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerInfo&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;schemars&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JsonSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Clone)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;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;h4&gt;
  
  
  Step 3 — Describe your inputs as types
&lt;/h4&gt;

&lt;p&gt;This is the part that sold me on the whole exercise. Your tool's input is a plain struct, and&lt;br&gt;
&lt;strong&gt;&lt;code&gt;schemars&lt;/code&gt; turns it into the JSON Schema the agent sees&lt;/strong&gt; — doc comments and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;JsonSchema)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;InstanceId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// EC2 instance id, e.g. `i-0123456789abcdef0`.&lt;/span&gt;
    &lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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;That doc comment becomes the field's &lt;code&gt;description&lt;/code&gt; in the tool schema. Rename the field and&lt;br&gt;
the schema follows. The compiler checks the type your handler destructures. There is no&lt;br&gt;
second artifact to keep in sync. ✅&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 4 — Write the tools
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;#[tool_router]&lt;/code&gt; on the impl block, &lt;code&gt;#[tool]&lt;/code&gt; on each method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tool_router(router&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;tool_router)]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tool_router&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="nd"&gt;#[tool(description&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"List EC2 instances tagged ManagedBy=gpu-vllm-g5g-2b."&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;list_g5g_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&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;let&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;BehaviorVersion&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;aws_sdk_ec2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="nf"&gt;.describe_instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tag:ManagedBy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpu-vllm-g5g-2b"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;Err&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"❌ describe_instances failed: {e}"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;res&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="nf"&gt;.reservations&lt;/span&gt;&lt;span class="p"&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;inst&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="nf"&gt;.instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"| `{}` | {} | {} |"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.instance_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.instance_type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&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="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown"&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"📡 Instances&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;| id | type | state |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;|---|---|---|&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&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;Tools that take arguments wrap them in &lt;code&gt;Parameters&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="nd"&gt;#[tool(description&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Terminate a G5g instance. Permanent — destroys the root volume."&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;terminate_g5g_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InstanceId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// …&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a small helper, since every tool returns the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ok&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;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;ContentBlock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — Implement ServerHandler
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;#[tool_handler]&lt;/code&gt; wires the router in, so you never write a dispatch &lt;code&gt;match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tool_handler(router&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;self&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;tool_router)]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ServerHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ServerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ServerInfo&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;ServerCapabilities&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enable_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.server_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"gpu-vllm-g5g-2b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_PKG_VERSION"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Devops agent for AWS EC2 G5g (Graviton2 + NVIDIA T4G) serving Gemma 4 &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             under vLLM. Remote administration goes through SSM; there is no inbound SSH."&lt;/span&gt;
                &lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&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;💡 These model structs are &lt;code&gt;#[non_exhaustive]&lt;/code&gt;, so use the constructors (&lt;code&gt;ServerInfo::new&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Implementation::new&lt;/code&gt;) and then assign fields — a struct literal won't compile, even with&lt;br&gt;
&lt;code&gt;..Default::default()&lt;/code&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 6 — main
&lt;/h4&gt;

&lt;p&gt;Four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;let&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;G5gServer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="nf"&gt;.waiting&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 7 — Test the protocol by hand
&lt;/h4&gt;

&lt;p&gt;An MCP server is a protocol implementation, so test it with a protocol transcript. Three&lt;br&gt;
JSON-RPC lines on stdin — no client 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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_help","arguments":{}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| ./target/release/gpu-vllm-g5g-2b-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;initialize OK: gpu-vllm-g5g-2b 0.1.0 proto 2026-07-28
&lt;/span&gt;&lt;span class="gp"&gt;tools/list OK: 9 tools -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;get_deployment_config, get_endpoint, get_help,
&lt;span class="go"&gt;  list_g5g_instances, run_remote, start_g5g_instance, stop_g5g_instance,
  terminate_g5g_instance, verify_model_health
tools/call get_help OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🟢 rmcp 3.1.2 negotiates the &lt;strong&gt;2026-07-28&lt;/strong&gt; spec version by default.&lt;/p&gt;

&lt;p&gt;Keep this snippet. It's the fastest way to tell "my server is broken" from "my client config&lt;br&gt;
is broken."&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 8 — Register it
&lt;/h4&gt;

&lt;p&gt;Point your MCP client at the binary. For Claude Code, &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"gpu-vllm-g5g-2b"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/abs/path/to/rust-mcp/target/release/gpu-vllm-g5g-2b-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"AWS_REGION"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server name prefixes every tool — &lt;code&gt;mcp__gpu-vllm-g5g-2b__list_g5g_instances&lt;/code&gt; — so name it&lt;br&gt;
after the thing it manages, especially if you run several.&lt;/p&gt;

&lt;p&gt;Credentials come from the standard AWS provider chain, so whatever&lt;br&gt;
&lt;code&gt;aws sts get-caller-identity&lt;/code&gt; resolves is what the server gets. Set &lt;code&gt;AWS_PROFILE&lt;/code&gt; to pick one.&lt;/p&gt;


&lt;h4&gt;
  
  
  Tests worth writing
&lt;/h4&gt;

&lt;p&gt;The interesting assertions aren't about the code, they're about the &lt;strong&gt;machine&lt;/strong&gt;. Turing has no&lt;br&gt;
bf16 datapath and no fp8, so the serving flags must differ from every L4-class box — exactly&lt;br&gt;
the sort of thing that silently reverts when someone copies a flag set from a neighbour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serve_flags_are_turing_shaped&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serve_flags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"google/gemma-4-E2B-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"g5g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"--dtype float16"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Turing has no bf16 datapath"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"--kv-cache-dtype auto"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Turing has no fp8 datapath"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"attention-backend"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// not a real vLLM v0.27 variable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;unknown_types_are_rejected_and_never_need_swap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_instance_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"t4g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// burstable CPU box, no GPU&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;needs_swap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"t4g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                      &lt;span class="c1"&gt;// 0 GiB must not read as "tiny"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second one earns its keep: &lt;code&gt;host_memory_gb&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt; for an unknown instance type,&lt;br&gt;
and a naive &lt;code&gt;ram &amp;lt; 16&lt;/code&gt; would decide an unrecognised machine needs a swapfile.&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;running 5 tests
&lt;/span&gt;&lt;span class="gp"&gt;test result: ok. 5 passed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0 failed&lt;span class="p"&gt;;&lt;/span&gt; finished &lt;span class="k"&gt;in &lt;/span&gt;0.00s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🐍 vs 🦀 — the scoreboard
&lt;/h4&gt;

&lt;p&gt;Cold start measured the way a client experiences it: spawn the process, send &lt;code&gt;initialize&lt;/code&gt; +&lt;br&gt;
&lt;code&gt;initialized&lt;/code&gt; + &lt;code&gt;tools/list&lt;/code&gt;, stop the clock when the tool list comes back. Seven runs, median.&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;🐍 Python (FastMCP)&lt;/th&gt;
&lt;th&gt;🦀 Rust (rmcp)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start to &lt;code&gt;tools/list&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;462.0 ms&lt;/strong&gt; (437–530)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.5 ms&lt;/strong&gt; (1.8–2.9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;83 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact&lt;/td&gt;
&lt;td&gt;Python runtime + 34 packages&lt;/td&gt;
&lt;td&gt;one binary, 39.4 MB (&lt;strong&gt;19.9 MB&lt;/strong&gt; stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct dependencies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total resolved packages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools implemented&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;759 lines&lt;/td&gt;
&lt;td&gt;560 + 52 of tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean release build&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;n/a&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 m 28 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;185x on cold start&lt;/strong&gt; — but per the section up top, resist quoting that on its own. One&lt;br&gt;
server spawned once a session, 460 ms, nobody notices. It only becomes a number worth having&lt;br&gt;
when you multiply it by sixteen rigs, and even then it's the &lt;strong&gt;12 MB vs 83 MB&lt;/strong&gt; row that does&lt;br&gt;
the heavier lifting.&lt;/p&gt;

&lt;p&gt;Read the rest honestly too. 241 resolved packages against 34 means the static binary is&lt;br&gt;
&lt;strong&gt;not&lt;/strong&gt; a smaller supply chain, just the same one audited in &lt;code&gt;Cargo.lock&lt;/code&gt;. The port covers 9&lt;br&gt;
tools to Python's 15 — the provisioning path (cloud-init rendering, AMI resolution, spot&lt;br&gt;
options) is the fiddly half and isn't ported. And 5 m 28 s of clean build against an&lt;br&gt;
interpreter that starts instantly is a real cost while you're iterating. 📊&lt;/p&gt;




&lt;h4&gt;
  
  
  So, worth it?
&lt;/h4&gt;

&lt;p&gt;For a single MCP server that already works: &lt;strong&gt;no&lt;/strong&gt;. Don't rewrite it.&lt;/p&gt;

&lt;p&gt;For sixteen of them sharing one system Python, shipped to machines that shouldn't need a&lt;br&gt;
Python environment at all: yes — and note that neither half of that sentence is about speed.&lt;br&gt;
It's a packaging answer.&lt;/p&gt;

&lt;p&gt;The part that'll still be true next year is &lt;code&gt;schemars&lt;/code&gt;. The tool schema the agent sees is&lt;br&gt;
generated from the same struct the handler destructures, checked by the compiler, documented&lt;br&gt;
by the doc comments on its fields. In the Python version the schema, the runtime types and the&lt;br&gt;
docs are three artifacts that agree by convention — and go quiet when they stop agreeing.&lt;/p&gt;

&lt;p&gt;The 462 ms is a bonus. The schema not being able to lie about the code is the reason. ✅&lt;/p&gt;




&lt;h4&gt;
  
  
  Cheat sheet
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# scaffold&lt;/span&gt;
cargo new &lt;span class="nt"&gt;--bin&lt;/span&gt; my-mcp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-mcp
cargo add rmcp &lt;span class="nt"&gt;--features&lt;/span&gt; server,macros,transport-io
cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; rt-multi-thread,macros
cargo add serde serde_json schemars anyhow

&lt;span class="c"&gt;# canonical examples for YOUR resolved version&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.cargo/registry/src/&lt;span class="k"&gt;*&lt;/span&gt;/rmcp-&lt;span class="k"&gt;*&lt;/span&gt;/tests/test_tool_macros.rs

&lt;span class="c"&gt;# build + smoke test&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p","version":"0"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
              &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
              &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | ./target/release/my-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four macros to remember: &lt;code&gt;#[tool_router]&lt;/code&gt; on the impl, &lt;code&gt;#[tool]&lt;/code&gt; on each method,&lt;br&gt;
&lt;code&gt;#[tool_handler]&lt;/code&gt; on the &lt;code&gt;ServerHandler&lt;/code&gt; impl, &lt;code&gt;Parameters&amp;lt;T&amp;gt;&lt;/code&gt; around your input struct.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Rust 1.97.1, rmcp 3.1.2, aws-sdk-ec2 1.246.0, edition 2024. Startup measured on the dev host&lt;br&gt;
— it's a comparison of two MCP servers, not a hardware result. Single machine, seven runs per&lt;br&gt;
side, median reported.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>mcp</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build an MCP server in Rust with rmcp: a walk-through 🦀</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:32:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/build-an-mcp-server-in-rust-with-rmcp-a-walk-through-41o3</link>
      <guid>https://dev.to/aws-builders/build-an-mcp-server-in-rust-with-rmcp-a-walk-through-41o3</guid>
      <description>&lt;p&gt;This tutorial walks through building an &lt;strong&gt;MCP server in Rust&lt;/strong&gt; with&lt;br&gt;
&lt;a href="https://crates.io/crates/rmcp" rel="noopener noreferrer"&gt;&lt;code&gt;rmcp&lt;/code&gt;&lt;/a&gt;, the official Model Context Protocol Rust SDK.&lt;/p&gt;

&lt;p&gt;The example is a real one: a devops agent that manages &lt;strong&gt;AWS EC2 G5g&lt;/strong&gt; instances — Graviton2&lt;br&gt;
boxes with NVIDIA T4G GPUs — serving Gemma 4 under vLLM. It launches instances, drives them&lt;br&gt;
over SSM, and health-checks the model. There's an existing Python version, so at the end we&lt;br&gt;
can put the two side by side.&lt;/p&gt;

&lt;p&gt;Follow along and you'll have a working, registerable MCP server. 🦀&lt;/p&gt;


&lt;h4&gt;
  
  
  Why Rust for this?
&lt;/h4&gt;

&lt;p&gt;Worth answering properly, because the weak version of the argument is easy to make and easy to&lt;br&gt;
demolish — and the real one is better anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with what it isn't: these tools are I/O bound.&lt;/strong&gt; Every one is an AWS API call —&lt;br&gt;
&lt;code&gt;describe_instances&lt;/code&gt;, &lt;code&gt;send_command&lt;/code&gt;, polling SSM — so 100–500 ms of network per call. The&lt;br&gt;
caller's language contributes nothing measurable there. Anyone selling you a Rust rewrite on&lt;br&gt;
raw speed for this workload is selling something.&lt;/p&gt;

&lt;p&gt;Three claims that don't hold, so nobody has to make them in the comments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Why it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"462 ms startup is slow"&lt;/td&gt;
&lt;td&gt;stdio servers spawn &lt;strong&gt;once per session&lt;/strong&gt;, not per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Rust is faster"&lt;/td&gt;
&lt;td&gt;the work is network round-trips to AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"smaller supply chain"&lt;/td&gt;
&lt;td&gt;241 crates vs 34 Python packages — it's &lt;em&gt;worse&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What actually justifies it, for this codebase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's a fleet, not a server.&lt;/strong&gt; This monorepo has &lt;strong&gt;16 rigs&lt;/strong&gt;, each with its own MCP&lt;br&gt;
server. That changes the units:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;All loaded together&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resident memory&lt;/td&gt;
&lt;td&gt;16 × 83 MB ≈ &lt;strong&gt;1.33 GB&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;16 × 12 MB ≈ &lt;strong&gt;192 MB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session startup&lt;/td&gt;
&lt;td&gt;16 × 462 ms ≈ &lt;strong&gt;7.4 s&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;16 × 2.5 ms ≈ &lt;strong&gt;40 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A gigabyte of resident Python to expose sixteen tool lists is a real cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No shared interpreter.&lt;/strong&gt; These rigs install system-wide — no virtualenvs, by policy — so&lt;br&gt;
all sixteen share one Python. Sixteen servers with independently drifting &lt;code&gt;boto3&lt;/code&gt; and &lt;code&gt;mcp&lt;/code&gt;&lt;br&gt;
pins in one interpreter is a standing conflict risk. A static binary has no such coupling;&lt;br&gt;
each rig pins whatever it likes in its own &lt;code&gt;Cargo.lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The schema can't drift from the code.&lt;/strong&gt; More on this at Step 3, but it's the one that&lt;br&gt;
survives longest: &lt;code&gt;schemars&lt;/code&gt; generates the tool schema from the same struct the handler&lt;br&gt;
destructures.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;distribution and correctness, not speed.&lt;/strong&gt; ✅ If you have one MCP server and it works,&lt;br&gt;
this is not a reason to rewrite it.&lt;/p&gt;


&lt;h4&gt;
  
  
  How does this all fit together?
&lt;/h4&gt;

&lt;p&gt;Two halves. The agent and the MCP server run on your machine; the GPU box is remote, and it&lt;br&gt;
has &lt;strong&gt;no inbound SSH&lt;/strong&gt; — everything goes through the AWS APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   YOUR MACHINE                                     AWS  us-east-1
┌──────────────────────────────┐       ┌───────────────────────────────────────┐
│                              │       │                                       │
│  Claude Code / IDE           │       │  ┌─ EC2 g5g.4xlarge ───────────────┐  │
│         |                    │       │  │  Graviton2 (aarch64)            │  │
│         | MCP · JSON-RPC 2.0 │       │  │  + NVIDIA T4G (SM 7.5)          │  │
│         | over stdio         │       │  │                                 │  │
│         v                    │  EC2  │  │  [PY] vLLM + [RUST] vllm-rs     │  │
│  ┌────────────────────────┐  │  API  │  │  listening on :8000             │  │
│  │ [RUST]                 │──┼──────&amp;gt;│  │                                 │  │
│  │ gpu-vllm-g5g-2b        │  │       │  │  Gemma 4 E2B                    │  │
│  │                        │  │  SSM  │  └─────────────────────────────────┘  │
│  │ rmcp 3.1.2             │──┼──────&amp;gt;│           ^                           │
│  │ tokio · schemars       │  │  Run  │           |  no inbound SSH,          │
│  │ aws-sdk-ec2 / -ssm     │  │  Cmd  │           |  no key pair,             │
│  │ 1 binary · 2.5 ms      │  │       │           |  no port 22 rule          │
│  └────────────────────────┘  │       │                                       │
│        9 tools               │       │  IAM instance profile carries         │
│  list / start / stop /       │       │  AmazonSSMManagedInstanceCore         │
│  terminate / endpoint /      │       │                                       │
│  run_remote / health ...     │       │                                       │
└──────────────────────────────┘       └───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent never talks to the GPU box directly. It calls a tool; the tool calls &lt;strong&gt;EC2&lt;/strong&gt; to&lt;br&gt;
manage the instance's lifecycle, or &lt;strong&gt;SSM Run Command&lt;/strong&gt; to execute something on it. That's&lt;br&gt;
what lets the box run with no inbound rules at all — which is the main reason this is worth&lt;br&gt;
building as a server rather than a pile of shell scripts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[RUST]&lt;/code&gt; on the right-hand side is vLLM's own Rust frontend — the other article in this&lt;br&gt;
series. This one is the &lt;code&gt;[RUST]&lt;/code&gt; on the left: the Rust that drives the box.&lt;/p&gt;


&lt;h4&gt;
  
  
  What is MCP, in one paragraph?
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Model Context Protocol&lt;/strong&gt; is how an AI agent discovers and calls your tools. Your server&lt;br&gt;
advertises a list of tools with JSON Schemas; the client (Claude Code, an IDE, whatever)&lt;br&gt;
calls them over JSON-RPC 2.0. Transport is usually &lt;strong&gt;stdio&lt;/strong&gt; — the client spawns your binary&lt;br&gt;
and talks over stdin/stdout.&lt;/p&gt;

&lt;p&gt;That last detail matters for the Rust pitch: if the client spawns your process on every&lt;br&gt;
session, &lt;strong&gt;process startup is a user-visible cost&lt;/strong&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 1 — Scaffold
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo new &lt;span class="nt"&gt;--bin&lt;/span&gt; rust-mcp &lt;span class="nt"&gt;--name&lt;/span&gt; gpu-vllm-g5g-2b-mcp
&lt;span class="nb"&gt;cd &lt;/span&gt;rust-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the dependencies. &lt;strong&gt;Feature flags are the thing to get right here&lt;/strong&gt; — &lt;code&gt;cargo add rmcp&lt;/code&gt;&lt;br&gt;
on its own compiles fine and gives you almost nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add rmcp &lt;span class="nt"&gt;--features&lt;/span&gt; server,macros,transport-io
&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;Feature&lt;/th&gt;
&lt;th&gt;What it brings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;ServerHandler&lt;/code&gt; trait and router types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macros&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;#[tool]&lt;/code&gt;, &lt;code&gt;#[tool_router]&lt;/code&gt;, &lt;code&gt;#[tool_handler]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transport-io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stdio transport&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The crate also ships &lt;code&gt;client&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;elicitation&lt;/code&gt;, &lt;code&gt;transport-streamable-http-server&lt;/code&gt; and&lt;br&gt;
more, all off by default. Add them when you need them.&lt;/p&gt;

&lt;p&gt;Then the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; rt-multi-thread,macros,process,time
cargo add serde serde_json anyhow schemars
cargo add aws-config aws-sdk-ec2 aws-sdk-ssm aws-sdk-secretsmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resulting &lt;code&gt;Cargo.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;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpu-vllm-g5g-2b-mcp"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;edition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2024"&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;rmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"macros"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transport-io"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.53.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rt-multi-thread"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"macros"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;aws-config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.10.1"&lt;/span&gt;
&lt;span class="py"&gt;aws-sdk-ec2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.246.0"&lt;/span&gt;
&lt;span class="py"&gt;aws-sdk-ssm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.118.0"&lt;/span&gt;
&lt;span class="py"&gt;serde&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.229"&lt;/span&gt;
&lt;span class="py"&gt;serde_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.151"&lt;/span&gt;
&lt;span class="py"&gt;schemars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.2.2"&lt;/span&gt;
&lt;span class="py"&gt;anyhow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.104"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🔎 Tip: where the canonical examples live
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;rmcp&lt;/code&gt; moves fast, and rendered docs lag. The &lt;strong&gt;vendored tests on your own disk&lt;/strong&gt; are&lt;br&gt;
compiled against the exact version you resolved:&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;ls&lt;/span&gt; ~/.cargo/registry/src/&lt;span class="k"&gt;*&lt;/span&gt;/rmcp-3.1.2/tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tests/test_tool_macros.rs&lt;/code&gt; is a complete, working server in about 60 lines. When an API&lt;br&gt;
question comes up, that file answers it faster and more reliably than anything else. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 2 — The server struct
&lt;/h4&gt;

&lt;p&gt;An rmcp server is a struct that owns a &lt;code&gt;ToolRouter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;rmcp&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServiceExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;server&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nn"&gt;model&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContentBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerCapabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerInfo&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;schemars&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JsonSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Clone)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;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;h4&gt;
  
  
  Step 3 — Describe your inputs as types
&lt;/h4&gt;

&lt;p&gt;This is the part that sold me on the whole exercise. Your tool's input is a plain struct, and&lt;br&gt;
&lt;strong&gt;&lt;code&gt;schemars&lt;/code&gt; turns it into the JSON Schema the agent sees&lt;/strong&gt; — doc comments and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;JsonSchema)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;InstanceId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// EC2 instance id, e.g. `i-0123456789abcdef0`.&lt;/span&gt;
    &lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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;That doc comment becomes the field's &lt;code&gt;description&lt;/code&gt; in the tool schema. Rename the field and&lt;br&gt;
the schema follows. The compiler checks the type your handler destructures. There is no&lt;br&gt;
second artifact to keep in sync. ✅&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 4 — Write the tools
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;#[tool_router]&lt;/code&gt; on the impl block, &lt;code&gt;#[tool]&lt;/code&gt; on each method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tool_router(router&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;tool_router)]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tool_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tool_router&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="nd"&gt;#[tool(description&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"List EC2 instances tagged ManagedBy=gpu-vllm-g5g-2b."&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;list_g5g_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&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;let&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;BehaviorVersion&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;aws_config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;aws_sdk_ec2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="nf"&gt;.describe_instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tag:ManagedBy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpu-vllm-g5g-2b"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;Err&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"❌ describe_instances failed: {e}"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;res&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="nf"&gt;.reservations&lt;/span&gt;&lt;span class="p"&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;inst&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="nf"&gt;.instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"| `{}` | {} | {} |"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.instance_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.instance_type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&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="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="nf"&gt;.state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown"&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"📡 Instances&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;| id | type | state |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;|---|---|---|&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&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;Tools that take arguments wrap them in &lt;code&gt;Parameters&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="nd"&gt;#[tool(description&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Terminate a G5g instance. Permanent — destroys the root volume."&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;terminate_g5g_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InstanceId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// …&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a small helper, since every tool returns the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ok&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;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;ContentBlock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — Implement ServerHandler
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;#[tool_handler]&lt;/code&gt; wires the router in, so you never write a dispatch &lt;code&gt;match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tool_handler(router&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;self&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;tool_router)]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ServerHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;G5gServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ServerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ServerInfo&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;ServerCapabilities&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enable_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.server_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"gpu-vllm-g5g-2b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_PKG_VERSION"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Devops agent for AWS EC2 G5g (Graviton2 + NVIDIA T4G) serving Gemma 4 &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             under vLLM. Remote administration goes through SSM; there is no inbound SSH."&lt;/span&gt;
                &lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;info&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;💡 These model structs are &lt;code&gt;#[non_exhaustive]&lt;/code&gt;, so use the constructors (&lt;code&gt;ServerInfo::new&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Implementation::new&lt;/code&gt;) and then assign fields — a struct literal won't compile, even with&lt;br&gt;
&lt;code&gt;..Default::default()&lt;/code&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 6 — main
&lt;/h4&gt;

&lt;p&gt;Four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;let&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;G5gServer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="nf"&gt;.waiting&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 7 — Test the protocol by hand
&lt;/h4&gt;

&lt;p&gt;An MCP server is a protocol implementation, so test it with a protocol transcript. Three&lt;br&gt;
JSON-RPC lines on stdin — no client 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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_help","arguments":{}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| ./target/release/gpu-vllm-g5g-2b-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;initialize OK: gpu-vllm-g5g-2b 0.1.0 proto 2026-07-28
&lt;/span&gt;&lt;span class="gp"&gt;tools/list OK: 9 tools -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;get_deployment_config, get_endpoint, get_help,
&lt;span class="go"&gt;  list_g5g_instances, run_remote, start_g5g_instance, stop_g5g_instance,
  terminate_g5g_instance, verify_model_health
tools/call get_help OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🟢 rmcp 3.1.2 negotiates the &lt;strong&gt;2026-07-28&lt;/strong&gt; spec version by default.&lt;/p&gt;

&lt;p&gt;Keep this snippet. It's the fastest way to tell "my server is broken" from "my client config&lt;br&gt;
is broken."&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 8 — Register it
&lt;/h4&gt;

&lt;p&gt;Point your MCP client at the binary. For Claude Code, &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"gpu-vllm-g5g-2b"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/abs/path/to/rust-mcp/target/release/gpu-vllm-g5g-2b-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"AWS_REGION"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server name prefixes every tool — &lt;code&gt;mcp__gpu-vllm-g5g-2b__list_g5g_instances&lt;/code&gt; — so name it&lt;br&gt;
after the thing it manages, especially if you run several.&lt;/p&gt;

&lt;p&gt;Credentials come from the standard AWS provider chain, so whatever&lt;br&gt;
&lt;code&gt;aws sts get-caller-identity&lt;/code&gt; resolves is what the server gets. Set &lt;code&gt;AWS_PROFILE&lt;/code&gt; to pick one.&lt;/p&gt;


&lt;h4&gt;
  
  
  Tests worth writing
&lt;/h4&gt;

&lt;p&gt;The interesting assertions aren't about the code, they're about the &lt;strong&gt;machine&lt;/strong&gt;. Turing has no&lt;br&gt;
bf16 datapath and no fp8, so the serving flags must differ from every L4-class box — exactly&lt;br&gt;
the sort of thing that silently reverts when someone copies a flag set from a neighbour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serve_flags_are_turing_shaped&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serve_flags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"google/gemma-4-E2B-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"g5g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"--dtype float16"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Turing has no bf16 datapath"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"--kv-cache-dtype auto"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Turing has no fp8 datapath"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"attention-backend"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// not a real vLLM v0.27 variable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;unknown_types_are_rejected_and_never_need_swap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_instance_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"t4g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// burstable CPU box, no GPU&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;needs_swap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"t4g.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                      &lt;span class="c1"&gt;// 0 GiB must not read as "tiny"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second one earns its keep: &lt;code&gt;host_memory_gb&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt; for an unknown instance type,&lt;br&gt;
and a naive &lt;code&gt;ram &amp;lt; 16&lt;/code&gt; would decide an unrecognised machine needs a swapfile.&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;running 5 tests
&lt;/span&gt;&lt;span class="gp"&gt;test result: ok. 5 passed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0 failed&lt;span class="p"&gt;;&lt;/span&gt; finished &lt;span class="k"&gt;in &lt;/span&gt;0.00s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🐍 vs 🦀 — the scoreboard
&lt;/h4&gt;

&lt;p&gt;Cold start measured the way a client experiences it: spawn the process, send &lt;code&gt;initialize&lt;/code&gt; +&lt;br&gt;
&lt;code&gt;initialized&lt;/code&gt; + &lt;code&gt;tools/list&lt;/code&gt;, stop the clock when the tool list comes back. Seven runs, median.&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;🐍 Python (FastMCP)&lt;/th&gt;
&lt;th&gt;🦀 Rust (rmcp)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start to &lt;code&gt;tools/list&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;462.0 ms&lt;/strong&gt; (437–530)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.5 ms&lt;/strong&gt; (1.8–2.9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;83 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact&lt;/td&gt;
&lt;td&gt;Python runtime + 34 packages&lt;/td&gt;
&lt;td&gt;one binary, 39.4 MB (&lt;strong&gt;19.9 MB&lt;/strong&gt; stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct dependencies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total resolved packages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools implemented&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;759 lines&lt;/td&gt;
&lt;td&gt;560 + 52 of tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean release build&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;n/a&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 m 28 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;185x on cold start&lt;/strong&gt; — but per the section up top, resist quoting that on its own. One&lt;br&gt;
server spawned once a session, 460 ms, nobody notices. It only becomes a number worth having&lt;br&gt;
when you multiply it by sixteen rigs, and even then it's the &lt;strong&gt;12 MB vs 83 MB&lt;/strong&gt; row that does&lt;br&gt;
the heavier lifting.&lt;/p&gt;

&lt;p&gt;Read the rest honestly too. 241 resolved packages against 34 means the static binary is&lt;br&gt;
&lt;strong&gt;not&lt;/strong&gt; a smaller supply chain, just the same one audited in &lt;code&gt;Cargo.lock&lt;/code&gt;. The port covers 9&lt;br&gt;
tools to Python's 15 — the provisioning path (cloud-init rendering, AMI resolution, spot&lt;br&gt;
options) is the fiddly half and isn't ported. And 5 m 28 s of clean build against an&lt;br&gt;
interpreter that starts instantly is a real cost while you're iterating. 📊&lt;/p&gt;




&lt;h4&gt;
  
  
  So, worth it?
&lt;/h4&gt;

&lt;p&gt;For a single MCP server that already works: &lt;strong&gt;no&lt;/strong&gt;. Don't rewrite it.&lt;/p&gt;

&lt;p&gt;For sixteen of them sharing one system Python, shipped to machines that shouldn't need a&lt;br&gt;
Python environment at all: yes — and note that neither half of that sentence is about speed.&lt;br&gt;
It's a packaging answer.&lt;/p&gt;

&lt;p&gt;The part that'll still be true next year is &lt;code&gt;schemars&lt;/code&gt;. The tool schema the agent sees is&lt;br&gt;
generated from the same struct the handler destructures, checked by the compiler, documented&lt;br&gt;
by the doc comments on its fields. In the Python version the schema, the runtime types and the&lt;br&gt;
docs are three artifacts that agree by convention — and go quiet when they stop agreeing.&lt;/p&gt;

&lt;p&gt;The 462 ms is a bonus. The schema not being able to lie about the code is the reason. ✅&lt;/p&gt;




&lt;h4&gt;
  
  
  Cheat sheet
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# scaffold&lt;/span&gt;
cargo new &lt;span class="nt"&gt;--bin&lt;/span&gt; my-mcp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-mcp
cargo add rmcp &lt;span class="nt"&gt;--features&lt;/span&gt; server,macros,transport-io
cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; rt-multi-thread,macros
cargo add serde serde_json schemars anyhow

&lt;span class="c"&gt;# canonical examples for YOUR resolved version&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.cargo/registry/src/&lt;span class="k"&gt;*&lt;/span&gt;/rmcp-&lt;span class="k"&gt;*&lt;/span&gt;/tests/test_tool_macros.rs

&lt;span class="c"&gt;# build + smoke test&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p","version":"0"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
              &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
              &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | ./target/release/my-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four macros to remember: &lt;code&gt;#[tool_router]&lt;/code&gt; on the impl, &lt;code&gt;#[tool]&lt;/code&gt; on each method,&lt;br&gt;
&lt;code&gt;#[tool_handler]&lt;/code&gt; on the &lt;code&gt;ServerHandler&lt;/code&gt; impl, &lt;code&gt;Parameters&amp;lt;T&amp;gt;&lt;/code&gt; around your input struct.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Rust 1.97.1, rmcp 3.1.2, aws-sdk-ec2 1.246.0, edition 2024. Startup measured on the dev host&lt;br&gt;
— it's a comparison of two MCP servers, not a hardware result. Single machine, seven runs per&lt;br&gt;
side, median reported.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>mcp</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>Shipping a vision-model verdict on Bedrock and Lightsail</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:24:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/shipping-a-vision-model-verdict-on-bedrock-and-lightsail-411</link>
      <guid>https://dev.to/aws-builders/shipping-a-vision-model-verdict-on-bedrock-and-lightsail-411</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 2026-08-15 against &lt;code&gt;us.amazon.nova-lite-v1:0&lt;/code&gt; via the Bedrock Converse API. FastAPI on Python 3.13, deployed to an Amazon Lightsail container service (&lt;code&gt;nano&lt;/code&gt;, scale 1) in &lt;code&gt;us-east-1&lt;/code&gt;. Scored against the live deployment, not localhost: 20/20 on the fixture set, median 880 ms per scan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://dog-or-not-lite.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com/" rel="noopener noreferrer"&gt;Dog or Not: Lite&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;Source: &lt;a href="https://github.com/xbill9/dog-or-not-lite" rel="noopener noreferrer"&gt;github.com/xbill9/dog-or-not-lite&lt;/a&gt;&lt;/strong&gt; · Built for the &lt;a href="https://builder.aws.com/content/3HkKlGRPcyks0rQpYVUVY9veCX0/weekend-challenge-build-a-creative-app" rel="noopener noreferrer"&gt;AWS Weekend Challenge: Build a Creative App&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make the model fill in a schema instead of writing a sentence.&lt;/strong&gt; The Converse API's &lt;code&gt;toolConfig&lt;/code&gt; plus &lt;code&gt;toolChoice&lt;/code&gt; forces a named function call, so &lt;code&gt;is_dog&lt;/code&gt; arrives as a boolean because it was declared as one. Every image comes back in the same shape — including the ambiguous ones, which is exactly where free-text output gets creative and a string-matching parser gets it wrong.&lt;/p&gt;

&lt;p&gt;The app is a webcam scanner that tells you whether the thing you are holding up is a dog. One HTML page, one &lt;code&gt;POST /api/scan&lt;/code&gt;, one model call, no build step, no framework. The whole backend is 285 lines.&lt;/p&gt;

&lt;p&gt;Three AWS specifics are worth the price of admission:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightsail container services have no IAM task role.&lt;/strong&gt; There is nothing to attach a policy to, so the container needs a real access key as an environment variable. The mitigation is scope, not secrecy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cross-region inference profile is authorized against every region it routes to.&lt;/strong&gt; With the policy pinned to &lt;code&gt;us-east-1&lt;/code&gt;, a call made &lt;em&gt;to&lt;/em&gt; &lt;code&gt;us-east-1&lt;/code&gt; was denied naming &lt;code&gt;us-west-2&lt;/code&gt;. Measured, not inferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--platform linux/amd64&lt;/code&gt; is not optional.&lt;/strong&gt; An arm64 image builds, pushes and deploys cleanly, then crash-loops with an exec format error that never mentions architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a mock mode that answers every scan locally is what made the frontend free to build — no credentials, no model access, no bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The shape: one route, one call
&lt;/h2&gt;

&lt;p&gt;The classification rule is the only opinionated part. &lt;code&gt;is_dog&lt;/code&gt; is true only for a living domestic dog: &lt;strong&gt;a wolf is not a dog&lt;/strong&gt;, nor is a coyote, fox, plush toy, bronze statue, cartoon, or person in a costume. That is a choice rather than a fact, and it is what makes the thing measurable — "is this a dog" is solved zero-shot by any modern vision model and has nothing to measure.&lt;/p&gt;

&lt;p&gt;The second rule keeps it usable: judge &lt;strong&gt;the subject depicted, never the medium carrying it&lt;/strong&gt;. People test this by holding a photo up on their phone, so a photograph of a real dog is a dog.&lt;/p&gt;

&lt;p&gt;Everything else is plumbing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser ──HTTPS──&amp;gt; Lightsail container service ──&amp;gt; Amazon Bedrock
 camera             (nano, 1 node, FastAPI)          Nova Lite
 or upload           serves the page AND              vision + tool use
                     the /api/scan route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two services total. One container, one model, one IAM user. No load balancer, no bucket, no API Gateway, no CDN to invalidate.&lt;/p&gt;

&lt;p&gt;Nova Lite is the cheapest Bedrock model that takes an image &lt;em&gt;and&lt;/em&gt; supports tool use, which is the exact intersection this needs. The &lt;code&gt;us.&lt;/code&gt; prefix matters: it is an inference profile, and in several regions Nova is only served through one. Invoking the bare &lt;code&gt;amazon.nova-lite-v1:0&lt;/code&gt; there fails with a &lt;code&gt;ValidationException&lt;/code&gt; that never mentions profiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Force the verdict into a schema
&lt;/h2&gt;

&lt;p&gt;Declare the tool, then require it. The schema is where the classification rule actually lives — the field descriptions do more work than the system prompt:&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;TOOL_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolSpec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;report_verdict&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;description&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;Report whether the subject presented to the &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scanner is a dog. Call this exactly once, 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;every image, always.&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;inputSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;object&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;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;boolean&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;description&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;True only for an actual living dog. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;False for a wolf, coyote, fox, plush &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toy, statue, drawing, cartoon or costume.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;integer&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;description&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;0-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;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;string&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;description&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;What it actually is, three words or fewer: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                       &lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;golden retriever&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grey wolf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                       &lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;ceramic figurine&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;boolean&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;description&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;True if the subject is a cat. Separate &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field because a cat is not merely a non-dog.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&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;confidence&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;subject&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;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="c1"&gt;# Without this, Nova will sometimes narrate instead of calling the tool.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolChoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;report_verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call itself, with the image as raw bytes — Converse takes &lt;code&gt;bytes&lt;/code&gt; directly, so no base64 round trip on this side:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-lite-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&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;jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;}}},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;Identify the subject.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}],&lt;/span&gt;
    &lt;span class="n"&gt;toolConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOL_CONFIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxTokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&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;Pulling the answer out is a loop over content blocks, not a regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolUse&lt;/span&gt;&lt;span class="sh"&gt;"&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;use&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;use&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model did not return a verdict&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;&lt;code&gt;toolChoice&lt;/code&gt; makes that last line close to unreachable. Keep it anyway — a 502 naming the cause beats a &lt;code&gt;KeyError&lt;/code&gt; traceback.&lt;/p&gt;

&lt;p&gt;Two things worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put the rule in the field description, not only the prompt.&lt;/strong&gt; The &lt;code&gt;is_dog&lt;/code&gt; description enumerating wolf/coyote/fox/plush/statue is read at the point of decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is_cat&lt;/code&gt; is a separate boolean, not a value of &lt;code&gt;subject&lt;/code&gt;.&lt;/strong&gt; Anything the UI branches on should be its own typed field. Parsing &lt;code&gt;subject == "tabby cat"&lt;/code&gt; to decide whether to show a different state is how you end up back in string-matching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Build the whole frontend with a mock
&lt;/h2&gt;

&lt;p&gt;One environment variable short-circuits the model call and cycles four canned verdicts:&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;MOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./run.sh          &lt;span class="c"&gt;# http://127.0.0.1:8080, no credentials, no bill&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MOCK_VERDICTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&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;golden retriever&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;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&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;grey wolf&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;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&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;tabby cat&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;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&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;plush dachshund&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;is_cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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;Pick the four deliberately: the happy path, the case that makes the rule interesting, the special state, and a low-confidence one. This is the only way to reach every UI state on demand rather than by going and finding a wolf.&lt;/p&gt;

&lt;p&gt;Build the client lazily or this mode does not work at all — constructing a boto3 client at import time fails on a machine that has never authenticated, which is precisely the machine &lt;code&gt;MOCK=1&lt;/code&gt; is 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;_bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_bedrock&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_bedrock&lt;/span&gt; &lt;span class="ow"&gt;is&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;_bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AWS_REGION&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;_bedrock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It paid for itself on one bug. The confidence meter had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;width&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="nt"&gt;s&lt;/span&gt; &lt;span class="nt"&gt;ease&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;background&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="nt"&gt;s&lt;/span&gt; &lt;span class="nt"&gt;ease&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;background&lt;/code&gt; transition left the bar showing the &lt;strong&gt;previous&lt;/strong&gt; verdict's colour indefinitely — green under &lt;code&gt;NOT A DOG&lt;/code&gt;, red under the cat state — while the verdict text, which had no transition, switched correctly. Two halves of the same readout disagreeing, permanently, each looking fine alone. You only see it if you can fire three verdicts in two seconds. A state indicator should snap to the state; only the fill level was ever worth animating.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Scope the IAM user, and expect a region you never asked for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lightsail container services cannot assume an IAM role.&lt;/strong&gt; On ECS or Lambda you attach a policy to a role and the SDK finds credentials. Lightsail has nothing to attach to, so the container gets a real key pair as environment variables — and deployment environment variables are readable afterwards through &lt;code&gt;get-container-services&lt;/code&gt;. There is no way to make that elegant. The honest response is to scope the key until it is boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./iam-setup.sh    &lt;span class="c"&gt;# creates the user, writes ~/dogornot-lite.key, chmod 600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It mints a &lt;strong&gt;new&lt;/strong&gt; access key every run and deletes the old ones, so a re-run rotates rather than fails. The policy is the interesting part:&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;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&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="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:*::foundation-model/amazon.nova-lite-v1:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:*:&amp;lt;ACCOUNT_ID&amp;gt;:inference-profile/us.amazon.nova-lite-v1:0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the wildcard regions, and note that you need &lt;strong&gt;both&lt;/strong&gt; ARNs. A cross-region inference profile routes across regions, and Bedrock authorizes &lt;code&gt;InvokeModel&lt;/code&gt; against the underlying foundation-model ARN in &lt;em&gt;each&lt;/em&gt; one. With the policy pinned to &lt;code&gt;us-east-1&lt;/code&gt;, a call made to &lt;code&gt;us-east-1&lt;/code&gt; fails like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AccessDeniedException: ... not authorized to perform: bedrock:InvokeModel
on resource: arn:aws:bedrock:us-west-2::foundation-model/amazon.nova-lite-v1:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;us-west-2&lt;/code&gt; was never requested and never configured. I only state this because I narrowed the policy on purpose to watch it fail and then restored it. If you are staring at a Bedrock denial naming a region you never asked for, this is why.&lt;/p&gt;

&lt;p&gt;One more denial worth naming: an &lt;code&gt;AccessDeniedException&lt;/code&gt; at the &lt;em&gt;first&lt;/em&gt; call, before any of the above, usually means Bedrock model access has not been granted for Nova in that account. The console calls it "Model access", and it is per-region.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploy: one container service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws login          &lt;span class="c"&gt;# or any credential source&lt;/span&gt;
./iam-setup.sh     &lt;span class="c"&gt;# one-time&lt;/span&gt;
./deploy.sh        &lt;span class="c"&gt;# build, push, deploy, print the URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy.sh&lt;/code&gt; is idempotent — re-running it ships a new deployment version to the same service on the same URL. Budget 5–10 minutes on the first run, most of it Lightsail provisioning the service before it will accept a deployment at all.&lt;/p&gt;

&lt;p&gt;Four things in it that are not obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build for x86 explicitly.&lt;/strong&gt; Lightsail nodes are amd64. On an arm64 laptop the default build deploys cleanly and then crash-loops with an exec format error that never says "architecture".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; dog-or-not-lite:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;lightsailctl&lt;/code&gt; is a separate binary.&lt;/strong&gt; &lt;code&gt;aws lightsail push-container-image&lt;/code&gt; is a thin wrapper around it and fails confusingly without it. Check with &lt;code&gt;command -v lightsailctl&lt;/code&gt;, not by looking in &lt;code&gt;/usr/local/bin&lt;/code&gt; — it may be installed under &lt;code&gt;~/.local/bin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the pushed image reference back from the API&lt;/strong&gt;, rather than scraping the push output, which prints it in prose:&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;IMAGE_REF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws lightsail get-container-images &lt;span class="nt"&gt;--service-name&lt;/span&gt; dog-or-not-lite &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'containerImages[0].image'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not be alarmed that the version is not sequential from 1. A brand-new service's first image came back as &lt;code&gt;:dog-or-not-lite.scanner.219&lt;/code&gt;. That is normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the secret off the command line.&lt;/strong&gt; Build the containers JSON with python so the key is JSON-escaped rather than shell-interpolated, write it to a &lt;code&gt;chmod 700&lt;/code&gt; tempdir, and pass it by reference — &lt;code&gt;ps&lt;/code&gt; never sees it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lightsail create-container-service-deployment &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service-name&lt;/span&gt; dog-or-not-lite &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--containers&lt;/span&gt; &lt;span class="s2"&gt;"file://&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/containers.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--public-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"file://&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/endpoint.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the health check at a route that exists (&lt;code&gt;/healthz&lt;/code&gt;, &lt;code&gt;intervalSeconds: 10&lt;/code&gt;, &lt;code&gt;healthyThreshold: 2&lt;/code&gt;). And if you serve static files from the same process, &lt;strong&gt;mount them last&lt;/strong&gt; — a &lt;code&gt;StaticFiles&lt;/code&gt; at &lt;code&gt;/&lt;/code&gt; mounted before your routes shadows every one of them:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StaticFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# must stay last
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Measure it before you believe it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/healthz&lt;/code&gt; passing proves the container booted, not that Bedrock is reachable from inside it. The only real verification is scoring the deployed URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./check.py &lt;span class="nt"&gt;--url&lt;/span&gt; https://&amp;lt;service&amp;gt;.us-east-1.cs.amazonlightsail.com &lt;span class="nt"&gt;--min-rate&lt;/span&gt; 0.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;20 images, each compared against a hand-checked &lt;code&gt;is_dog&lt;/code&gt; in &lt;code&gt;fixtures/fixtures.json&lt;/code&gt;. Stdlib only, so it runs against a deployed URL from anywhere without installing anything. Two details make it worth having:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fixtures are stored at 640×480 q70 — the exact format the browser sends.&lt;/strong&gt; The harness exercises the same payload the real client does, not a pristine 4000px original the app will never see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The expectations were verified by eye before being committed.&lt;/strong&gt; Generate the input, never the expectation. An eval whose ground truth came out of a model is measuring agreement, not accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Against the deployed service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median latency&lt;/td&gt;
&lt;td&gt;880 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wolves, foxes, statues, cats&lt;/td&gt;
&lt;td&gt;all classified correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breeds named unprompted&lt;/td&gt;
&lt;td&gt;"beagle dog", "corgi dog", "german shepherd"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Read that as a smoke test, not a benchmark.&lt;/strong&gt; Twenty clean, well-lit, subject-fills-frame images say the prompt works and the plumbing is right. They say very little about a dog photographed badly at dusk, and I would not claim from this that the rule is robust.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What it costs, and how to turn it off
&lt;/h2&gt;

&lt;p&gt;The standing cost is the container service, not the model. A Lightsail container service bills for as long as it exists, whether or not anyone uses it — roughly &lt;strong&gt;$7/month at &lt;code&gt;nano&lt;/code&gt;&lt;/strong&gt;. Deleting the deployment does not stop that. You have to delete the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lightsail delete-container-service &lt;span class="nt"&gt;--service-name&lt;/span&gt; dog-or-not-lite
aws iam delete-user-policy &lt;span class="nt"&gt;--user-name&lt;/span&gt; dog-or-not-lite &lt;span class="nt"&gt;--policy-name&lt;/span&gt; InvokeNovaLite
aws iam list-access-keys &lt;span class="nt"&gt;--user-name&lt;/span&gt; dog-or-not-lite   &lt;span class="c"&gt;# delete each, then:&lt;/span&gt;
aws iam delete-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; dog-or-not-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-scan Bedrock cost is small by comparison — a 640×480 q70 JPEG lands at 40–60 KB — but I have not metered it over a long enough run to publish a number. The app logs &lt;code&gt;inputTokens&lt;/code&gt; and &lt;code&gt;outputTokens&lt;/code&gt; on every scan if you want your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Reproduction
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/dog-or-not-lite
&lt;span class="nb"&gt;cd &lt;/span&gt;dog-or-not-lite
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Frontend work: no credentials, no model access, no bill&lt;/span&gt;
&lt;span class="nv"&gt;MOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./run.sh                      &lt;span class="c"&gt;# http://127.0.0.1:8080&lt;/span&gt;

&lt;span class="c"&gt;# Real model calls locally (needs Bedrock model access for Nova in your region)&lt;/span&gt;
./run.sh
./check.py                           &lt;span class="c"&gt;# score 20 fixtures against localhost&lt;/span&gt;

&lt;span class="c"&gt;# Deploy&lt;/span&gt;
./iam-setup.sh                       &lt;span class="c"&gt;# scoped IAM user + key at ~/dogornot-lite.key&lt;/span&gt;
./deploy.sh                          &lt;span class="c"&gt;# build (amd64) → push → deploy → print URL&lt;/span&gt;
./check.py &lt;span class="nt"&gt;--url&lt;/span&gt; &amp;lt;printed url&amp;gt; &lt;span class="nt"&gt;--min-rate&lt;/span&gt; 0.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat if you use &lt;code&gt;aws login&lt;/code&gt; locally: those credentials require &lt;code&gt;botocore[crt]&lt;/code&gt;, which is deliberately not in &lt;code&gt;requirements.txt&lt;/code&gt;. Without it boto3 reports &lt;code&gt;Missing Dependency: Using the login credential provider requires ... botocore[crt]&lt;/code&gt; and the app returns &lt;code&gt;502 bedrock unreachable&lt;/code&gt;. Install it in your virtualenv only — the container authenticates with a static key pair, which needs no CRT, and adding it would put ~20 MB in the image for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://dog-or-not-lite.6wpv8vensby5c.us-east-1.cs.amazonlightsail.com/" rel="noopener noreferrer"&gt;Dog or Not: Lite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/xbill9/dog-or-not-lite" rel="noopener noreferrer"&gt;github.com/xbill9/dog-or-not-lite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built for:&lt;/strong&gt; &lt;a href="https://builder.aws.com/content/3HkKlGRPcyks0rQpYVUVY9veCX0/weekend-challenge-build-a-creative-app" rel="noopener noreferrer"&gt;AWS Weekend Challenge: Build a Creative App&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Fixture images from Wikimedia Commons, attributed in the repository. Bark sound effects generated with &lt;a href="https://elevenlabs.io" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a security scanner that checks if you are a dog</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:11:53 +0000</pubDate>
      <link>https://dev.to/gde/i-built-a-security-scanner-that-checks-if-you-are-a-dog-357n</link>
      <guid>https://dev.to/gde/i-built-a-security-scanner-that-checks-if-you-are-a-dog-357n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I went looking for a dog in the GPU family tree. I found fish, dead physicists, and a bridge in Florence.&lt;/p&gt;

&lt;p&gt;NVIDIA names its architectures after scientists — Tesla, Kepler, Hopper, Blackwell. AMD names its GPUs after &lt;em&gt;fish&lt;/em&gt;: Sienna Cichlid, Navy Flounder, Hotpink Bonefish. Intel uses Italian bridges. There is exactly one dog in the entire lineage: &lt;strong&gt;Husky&lt;/strong&gt;, the CPU cores in AMD's 2011 Llano APU. And because Llano was an APU, those Husky cores shared a die with an integrated Radeon — codenamed &lt;strong&gt;Sumo&lt;/strong&gt;. The closest a dog has ever gotten to a GPU is sitting next to one, and even then the graphics half got named after a wrestler.&lt;/p&gt;

&lt;p&gt;So I put one there myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dog or Not&lt;/strong&gt; is a live-video security scanner with one job. You hold something up to the camera, say &lt;strong&gt;"scan"&lt;/strong&gt;, and it tells you whether it is a dog. If it is, it barks.&lt;/p&gt;

&lt;p&gt;It is deliberately not charming about it. The scanner is a cold threat-assessment system that happens to have been pointed at dogs, and the entire joke is that it does not know it is making one. Hold up a golden retriever and it says, in the flattest voice available:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Woof."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hold up a wolf and it says &lt;em&gt;"Negative. Grey wolf."&lt;/em&gt; Hold up a cat and it suffers a fatal system error. Get three dogs in frame and it declares a &lt;strong&gt;containment breach&lt;/strong&gt; — in whichever of nine languages you picked.&lt;/p&gt;

&lt;p&gt;The classification line is where it gets interesting. A wolf is not a dog. Neither is a coyote, a fox, a plush toy, a bronze statue, a cartoon, or a person in a costume. That is a choice rather than a fact, and it is the choice that makes the thing measurable — "is this a dog" is otherwise solved zero-shot and there is nothing to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://dog-or-not-289270257791.us-central1.run.app" rel="noopener noreferrer"&gt;https://dog-or-not-289270257791.us-central1.run.app&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%2Fuodbpn3bq9emaa8gwdh1.gif" 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%2Fuodbpn3bq9emaa8gwdh1.gif" alt="A phone showing a dog photo held up to the webcam. The scanner returns a green DOG verdict reading " width="600" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grant camera access, press INITIATE, then hold something up and say &lt;strong&gt;"scan"&lt;/strong&gt;.&lt;br&gt;
Chrome or Edge for the voice command — everywhere else, use the SCAN button,&lt;br&gt;
which does exactly the same thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/xbill9" rel="noopener noreferrer"&gt;
        xbill9
      &lt;/a&gt; / &lt;a href="https://github.com/xbill9/devto-dog" rel="noopener noreferrer"&gt;
        devto-dog
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A live-video security scanner that checks whether you are holding up a dog, and barks if you are. DEV Weekend Challenge: Dog Days Edition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Dog or Not&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A live-video security scanner with one job: you hold something up to the camera
say &lt;strong&gt;"scan"&lt;/strong&gt;, and it tells you whether it is a dog. If it is, it barks.&lt;/p&gt;
&lt;p&gt;It is deliberately not charming about it. The scanner is a cold
threat-assessment system that happens to have been pointed at dogs, and the
entire joke is that it does not know it is making one.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/weekend-2026-08-13" rel="nofollow"&gt;DEV Weekend Challenge: Dog Days Edition&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://dog-or-not-289270257791.us-central1.run.app" rel="nofollow noopener noreferrer"&gt;https://dog-or-not-289270257791.us-central1.run.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Grant camera access, press INITIATE, hold something up and say &lt;strong&gt;"scan"&lt;/strong&gt;
Chrome or Edge for the voice command; everywhere else the SCAN button does the
same thing.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Credit where it is due&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;This is a fork of &lt;strong&gt;&lt;a href="https://github.com/xbill9/way-back-home" rel="noopener noreferrer"&gt;way-back-home/level_3_new&lt;/a&gt;&lt;/strong&gt;
which was the same scanner counting fingers for a biometric handshake. The
multimodal plumbing — bidirectional WebSocket, 1 FPS video, local wake-word
detection, the accuracy harness, the Cloud…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/xbill9/devto-dog" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Running it yourself is four commands, and only one of them costs anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/install_deps.sh      &lt;span class="c"&gt;# NOT a bare pip install -r; see below&lt;/span&gt;
make frontend                  &lt;span class="c"&gt;# npm ci &amp;amp;&amp;amp; vite build — the backend serves dist/&lt;/span&gt;
make mock                      &lt;span class="c"&gt;# offline fake server on :8080, free, no API key&lt;/span&gt;
make run                       &lt;span class="c"&gt;# the real Live API — this one bills&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dependency script exists because &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; hard-fails here with &lt;code&gt;ResolutionImpossible&lt;/code&gt;. &lt;code&gt;requirements.txt&lt;/code&gt; pins &lt;code&gt;websockets==17.0.1&lt;/code&gt;, deliberately above the caps &lt;code&gt;google-adk&lt;/code&gt; (&lt;code&gt;&amp;lt;16&lt;/code&gt;) and &lt;code&gt;google-genai&lt;/code&gt; (&lt;code&gt;&amp;lt;17&lt;/code&gt;) declare — those bounds are "last version we tested", not a real incompatibility, and &lt;code&gt;overrides.txt&lt;/code&gt; overrides them. &lt;code&gt;uv&lt;/code&gt; applies it with &lt;code&gt;--override&lt;/code&gt;; pip has no equivalent, so the script installs the tree without websockets and then forces the pin with &lt;code&gt;--no-deps&lt;/code&gt;. Do not "fix" this with a bare &lt;code&gt;--no-deps&lt;/code&gt; on the whole file: it skips every transitive dependency too and leaves an app that cannot import.&lt;/p&gt;

&lt;p&gt;Forked from &lt;a href="https://github.com/xbill9/way-back-home" rel="noopener noreferrer"&gt;way-back-home&lt;/a&gt;, which was the same scanner counting fingers for a biometric handshake. The multimodal plumbing came from there — bidirectional WebSocket, 1 FPS video, local wake-word detection, the accuracy harness, the Cloud Run chain — and it is the reason this exists at all in a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  So where does the model actually sit?
&lt;/h3&gt;

&lt;p&gt;Nothing here is a request/response call. The browser holds one WebSocket open for the life of a scan, and the same socket carries frames up and speech down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (Chrome / Edge)                        Cloud Run  --min-instances=1
┌─────────────────────────────┐                ┌──────────────────────────────┐
│ getUserMedia                │                │ FastAPI  main.py             │
│  ├ video → canvas → JPEG    │  binary  2  →  │  ├ origin allowlist          │
│  │    640×480 q60 @ 1 FPS   │                │  │   (CORS does not apply    │
│  └ audio → Web Speech API   │  {"type":      │  │    to WebSockets)         │
│      never leaves the tab   │   "text"}  →   │  ├ LiveRequestQueue          │
│                             │                │  └ ADK Runner  run_live()    │
│ audioStreamer.js            │  ←  binary  3  │           │                  │
│  └ mu-law → PCM → worklet   │     (mu-law)   │           ▼                  │
│                             │                │  biometric_agent             │
│ DogScanner.jsx              │  ←  {"type":   │   report_verdict             │
│  └ DOG / NOT A DOG          │      "match"}  │   trigger_system_error       │
└─────────────────────────────┘                │   trigger_heavy_metal_mode   │
                                               └──────────────┬───────────────┘
                                                              │ bidiGenerateContent
                                                              ▼
                                                     Gemini Live API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions in that picture cost the most to arrive at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The microphone does not stream.&lt;/strong&gt; Its entire job is to catch the word "scan", and doing that over the wire costs 256 kbit/s of raw PCM — about two thirds of the uplink. Worse, continuous audio is what &lt;em&gt;stops&lt;/em&gt; the model taking turns: measured &lt;strong&gt;0/5&lt;/strong&gt; with speech in the room against &lt;strong&gt;5/5&lt;/strong&gt; for the identical prompts sent as text. So the audio stays in the browser and the wake word sends the same text frame the offline harness sends. Uplink drops from ~385 to ~128 kbit/s, all of it video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model audio comes back as mu-law on a binary frame, not base64 in the JSON.&lt;/strong&gt; Base64 inflates binary by a third, and over a 14-second session that was measured at 220 KB → 95 KB, downlink 128 → 55 kbit/s. The encoder is 40 lines of pure Python; the decoder is a 256-entry table in the browser. Change one and you must change the other — a mismatch is static, not an error.&lt;/p&gt;

&lt;p&gt;I did not write this. A self-paced loop did, and I want to talk about what that was actually like — because the interesting part is not that it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loop-driven development&lt;/strong&gt;, concretely: instead of a conversation, the agent schedules its own next wake-up. It reads a build log, picks the highest-priority unblocked task, does it, and appends an honest entry about what moved and what broke. Then it decides when to come back — fifteen minutes while there was work, thirty once it ran out — and arms a file watcher so it wakes immediately if the thing it is waiting on arrives.&lt;/p&gt;

&lt;p&gt;That build log is in the repo, tick by tick, written as it happened rather than reconstructed afterwards. This section is drawn from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every green checkmark was, at some point, green over something broken
&lt;/h3&gt;

&lt;p&gt;This is the thing I did not expect, and it is the whole reason the post is worth reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;35 passing tests over an app that could not work.&lt;/strong&gt; The backend started sending &lt;code&gt;{is_dog, subject, confidence}&lt;/code&gt;. The tests went green. The frontend was still reading &lt;code&gt;msg.count || msg.digit&lt;/code&gt; off that same frame, so the verdict never reached the UI. Nothing covers that seam — the Python suite stops at the socket and there were no frontend tests at all. The tests measured exactly what they cover, which was not the broken part.&lt;/p&gt;

&lt;p&gt;There was a nastier bug hiding in the same line. &lt;code&gt;is_dog&lt;/code&gt; is a &lt;em&gt;boolean&lt;/em&gt;, so the old &lt;code&gt;msg.count || ...&lt;/code&gt; idiom would have silently discarded every NOT-A-DOG verdict — half the answers, and the more interesting half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean lint over a failed build.&lt;/strong&gt; Deleting a dead function took a live one out with it, four lines below. ESLint passed — it does not resolve cross-module imports by default — and the failure only appeared in &lt;code&gt;vite build&lt;/code&gt;, buried under twelve lines of rollup stack trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A green test asserting the exact thing it was supposed to prevent.&lt;/strong&gt; The project keeps a non-public model id out of the repo, enforced by a gate wired into &lt;code&gt;make test&lt;/code&gt; and &lt;code&gt;make deploy&lt;/code&gt;. One test asserted that id as a string literal. A leak with a checkmark on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A gate that caught itself.&lt;/strong&gt; That same gate failed on its first run — on its own list of patterns, which necessarily contains every string it searches for. Funny once, an infinite loop thereafter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same bug, three times, in three places
&lt;/h3&gt;

&lt;p&gt;A route that exists in the real backend and not in the place people actually develop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/api/config&lt;/code&gt; 404ing under the Vite dev proxy, which forwarded only &lt;code&gt;/ws&lt;/code&gt;. Symptom: the header read AWAITING LINK — &lt;em&gt;the exact bug that endpoint was written to fix.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/config&lt;/code&gt; and &lt;code&gt;/api/fixtures&lt;/code&gt; missing from the mock server — the documented way to work on the UI without billing a session. The fixture portal could not load a single image there.&lt;/li&gt;
&lt;li&gt;Waiting to happen on the next endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every one failed &lt;strong&gt;soft&lt;/strong&gt;. Nothing crashed, nothing logged an error, and the feature just looked unfinished. The graceful fallback hid its own cause. There is now a test that diffs the real app's &lt;code&gt;/api/*&lt;/code&gt; routes against the mock's and fails naming the difference — and I checked it is not vacuous, because two empty sets compare equal and a parity test over nothing passes forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model that narrates calling a tool and then doesn't
&lt;/h3&gt;

&lt;p&gt;The whole architecture hangs off one tool call: the model sees a subject, calls &lt;code&gt;report_verdict(is_dog, confidence, subject)&lt;/code&gt;, and everything downstream — UI, bark, scoring — follows from that.&lt;/p&gt;

&lt;p&gt;Exactly one non-preview model offers the Live API. On it, every single trial came back SILENT. Meanwhile its own thinking said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A visual identification scan reveals a clear image of a Golden Retriever. The subject is verified as a real dog with 95% confidence, as &lt;code&gt;is_dog&lt;/code&gt; is true... I'm executing the &lt;code&gt;report_verdict&lt;/code&gt; tool with the dog's details, then I will say 'Woof.'"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It sees the dog. It narrates the call. &lt;strong&gt;Zero tool calls, ever.&lt;/strong&gt; Meanwhile the fallback model inherited from the earlier build could not open a Live session at all, under a comment claiming it could — because until the first real session, nothing had ever opened one.&lt;/p&gt;

&lt;h3&gt;
  
  
  One fixture in twenty was poisoned
&lt;/h3&gt;

&lt;p&gt;The eval set came from Wikimedia Commons, sourced by search term. One of them — "Dog with Goofy plush toy" — is a &lt;strong&gt;real dog chewing a toy&lt;/strong&gt;, filed as not-a-dog. It would have scored every correct answer as a failure.&lt;/p&gt;

&lt;p&gt;There is no test for that. The only way to catch it is to look at it. The project's own older documentation already says so: &lt;em&gt;generate the input, never the expectation.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  And the harness was scoring obedience as failure
&lt;/h3&gt;

&lt;p&gt;One dog fixture came back SILENT twice, consistently. It is a Halloween dog park — beagle, cavalier, retriever in the background — and the model had called &lt;code&gt;trigger_heavy_metal_mode()&lt;/code&gt;. &lt;strong&gt;The containment breach fired unprompted, on an unposed real-world photograph.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The harness only scored &lt;code&gt;report_verdict&lt;/code&gt;. But the instruction gives the easter eggs &lt;em&gt;absolute priority&lt;/em&gt; over &lt;code&gt;report_verdict&lt;/code&gt; — so the harness was marking the model doing exactly as it was told as a failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it actually right, though?
&lt;/h3&gt;

&lt;p&gt;There is exactly one thing in this repo that measures the model rather than the plumbing, and it needs no human in front of the camera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/scan_accuracy.py &lt;span class="nt"&gt;--blur-prob&lt;/span&gt; 0.3 &lt;span class="nt"&gt;--jitter&lt;/span&gt; 2 &lt;span class="nt"&gt;--min-rate&lt;/span&gt; 0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It drives the real deployed endpoint with fixture images at 640×480 JPEG q60 — the exact format the browser sends — puts a &lt;code&gt;{"type":"text"}&lt;/code&gt; frame in where you would say "scan", and scores every &lt;code&gt;report_verdict&lt;/code&gt; against a count verified by eye before it was committed. One billed session per run. &lt;code&gt;--blur-prob&lt;/code&gt; and &lt;code&gt;--jitter&lt;/code&gt; approximate a real webcam; &lt;code&gt;--min-rate&lt;/code&gt; turns it into a gate that can fail a build.&lt;/p&gt;

&lt;p&gt;What it cannot see: it holds one static fixture per trial, so it is structurally incapable of catching a &lt;em&gt;sampling&lt;/em&gt; miss — a pose held for less than a second at 1 FPS that lands in no frame at all. That blind spot is how "1 FPS and 2 FPS score identically" got measured, believed, and written down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The numbers
&lt;/h3&gt;

&lt;p&gt;Twenty fixtures, one session each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;correct verdict&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;six breeds, wolves, coyotes, foxes, two bronze statues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;containment breach&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;correct — three dogs in frame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat alarm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;both cats triggered the system error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;called a non-dog a dog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;refused a real dog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Latency 0.68–1.56s, mostly around 0.7. It correctly said &lt;em&gt;"bronze statue"&lt;/em&gt; for a sculpture of a man with a bronze dog beside him.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the loop got wrong
&lt;/h3&gt;

&lt;p&gt;It reported the wrong blocker for four hours. It kept saying it was waiting on dog photos, while (a) there was no API key, which it had never checked, and (b) twenty public-domain fixtures with generated attribution turned out to be about ten minutes of work.&lt;/p&gt;

&lt;p&gt;It optimised what it could reach instead of what mattered, and reported a blocker it had not verified. That is a very human failure mode and it is worth naming, because the honest version of "I let an AI build this over a weekend" includes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Google AI&lt;/strong&gt; — Gemini Live via the Agent Development Kit. Bidirectional WebSocket streaming video at 1 FPS, structured verdicts through tool calls rather than parsed prose, and a nine-language session config where the model translates its own lines rather than reading a shipped phrasebook. That last part is the cheapest possible proof the model is really being called: a recording cannot answer in Japanese.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — the bark pack, generated with the Sound Effects API at &lt;strong&gt;build time&lt;/strong&gt;, never at runtime. The clips are fetched and decoded once and held in memory, so the bark adds zero latency to the response path and cannot fail during a session. This project had already measured what a second audio stream does to a Live session — 0/5 against 5/5 — and the way to use a sound API here was to make the app touch the network &lt;em&gt;less&lt;/em&gt;, not more.&lt;/p&gt;

&lt;p&gt;Sound effects generated with &lt;a href="https://elevenlabs.io" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt;. Fixture images from Wikimedia Commons, attributed in &lt;code&gt;tests/fixtures/ATTRIBUTION.md&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Measured on:&lt;/strong&gt; &lt;code&gt;gemini-3.1-flash-live-preview&lt;/code&gt; via &lt;code&gt;google-adk==2.6.3&lt;/code&gt; / &lt;code&gt;google-genai==2.17.0&lt;/code&gt;, Python 3.13, deployed to Cloud Run in &lt;code&gt;us-central1&lt;/code&gt; with &lt;code&gt;--min-instances=1&lt;/code&gt; and &lt;code&gt;--timeout=3600&lt;/code&gt; (a WebSocket is one long request; the default 300s cap would end every session at five minutes). Video 640×480 JPEG q60 at 1 FPS, audio 16 kHz PCM up / 24 kHz down. Client tested in Chrome — the wake word is &lt;code&gt;SpeechRecognition&lt;/code&gt;, which is Chrome and Edge only; everywhere else the SCAN button does the identical thing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Serving Gemma4 with Rust on vLLM 🦀</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 20:43:34 +0000</pubDate>
      <link>https://dev.to/gde/serving-gemma4-with-rust-for-vllm-372l</link>
      <guid>https://dev.to/gde/serving-gemma4-with-rust-for-vllm-372l</guid>
      <description>&lt;p&gt;This tutorial walks through installing and setting up the &lt;strong&gt;Rust toolchain for vLLM&lt;/strong&gt; on an&lt;br&gt;
AWS EC2 &lt;strong&gt;G5g&lt;/strong&gt; instance — Graviton2 (aarch64) with an NVIDIA T4G GPU — and getting vLLM's&lt;br&gt;
Rust frontend (&lt;code&gt;vllm-rs&lt;/code&gt;) built, running, and &lt;em&gt;verified&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This paper is a follow-on to the original G5g Gemma 4 build.&lt;/p&gt;

&lt;p&gt;Everything below was run on the box. 🦀&lt;/p&gt;


&lt;h4&gt;
  
  
  Wait, vLLM has Rust in it?
&lt;/h4&gt;

&lt;p&gt;You betcha. Since &lt;a href="https://github.com/vllm-project/vllm/pull/40848" rel="noopener noreferrer"&gt;PR #40848&lt;/a&gt; (merged&lt;br&gt;
2026-05-21), vLLM vendors a &lt;strong&gt;14-crate Rust workspace&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;bench  chat  cmd  engine-core-client  llm  managed-engine  metrics
mock-engine  parser  parser/python  server  text  tokenizer  tracing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edition 2024, resolver 3. Straight from the vendored &lt;code&gt;rust/Cargo.toml&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;Crate&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;axum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8.8&lt;/td&gt;
&lt;td&gt;the HTTP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.47.1&lt;/td&gt;
&lt;td&gt;async runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zeromq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6.0&lt;/td&gt;
&lt;td&gt;talks to the Python engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rmp-serde&lt;/code&gt; / &lt;code&gt;rmpv&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.3.1&lt;/td&gt;
&lt;td&gt;msgpack on the wire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minijinja&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.22&lt;/td&gt;
&lt;td&gt;chat templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tonic&lt;/code&gt; / &lt;code&gt;prost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.14.6 / 0.14.3&lt;/td&gt;
&lt;td&gt;gRPC — &lt;strong&gt;remember this one&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's a drop-in replacement for the Python FastAPI server. Two artifacts get built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🦀 &lt;strong&gt;&lt;code&gt;vllm-rs&lt;/code&gt;&lt;/strong&gt; — the axum frontend binary&lt;/li&gt;
&lt;li&gt;🐍 &lt;strong&gt;&lt;code&gt;vllm._rust_tool_parser&lt;/code&gt;&lt;/strong&gt; — a PyO3 extension module&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Rust is a build requirement now
&lt;/h4&gt;

&lt;p&gt;That's the headline, and it's reason enough on its own: &lt;strong&gt;you cannot build vLLM from source at&lt;br&gt;
v0.27.2rc0 without Rust in the picture.&lt;/strong&gt; &lt;code&gt;setup.py&lt;/code&gt; imports it at module scope, line 21,&lt;br&gt;
unguarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;setuptools_rust.build&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build_rust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;try&lt;/code&gt;, no feature flag, no opt-out. Metadata generation doesn't happen without it.&lt;/p&gt;

&lt;p&gt;And this isn't a quirk of one release. vLLM's Rust surface is &lt;strong&gt;14 crates&lt;/strong&gt; covering the HTTP&lt;br&gt;
frontend, the tool parser, the tokenizer and the benchmark client, and it has been growing&lt;br&gt;
since it landed. If you build inference infrastructure from source, a Rust toolchain is&lt;br&gt;
becoming table stakes — so it's worth knowing how to drive it properly rather than working&lt;br&gt;
around it.&lt;/p&gt;

&lt;p&gt;Three things do get conflated, though, and they have different scopes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Needed to build vLLM?&lt;/th&gt;
&lt;th&gt;Needed to serve?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;setuptools_rust&lt;/code&gt; (Python pkg)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes, always&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cargo&lt;/code&gt; / &lt;code&gt;rustc&lt;/code&gt; toolchain&lt;/td&gt;
&lt;td&gt;for working Rust artifacts&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;protoc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;for &lt;code&gt;vllm-rs&lt;/code&gt; specifically&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Then why doesn't &lt;code&gt;pip install vllm&lt;/code&gt; need this?
&lt;/h4&gt;

&lt;p&gt;Because normally pip installs it for you. &lt;code&gt;pyproject.toml&lt;/code&gt; declares it:&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;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"cmake&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.26&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;ninja&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;packaging&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;24.2&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;77.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;81.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;setuptools-scm&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools-rust&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",          # &amp;lt;- pip grabs this automatically&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"torch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.13&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",                 # &amp;lt;- ...and this. Which is the problem.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="s"&gt;"wheel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jinja2"&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;Under normal &lt;strong&gt;build isolation&lt;/strong&gt;, pip creates a clean env, installs that list, and builds.&lt;br&gt;
You never see &lt;code&gt;setuptools_rust&lt;/code&gt; because you never had to think about it.&lt;/p&gt;

&lt;p&gt;But look at the &lt;code&gt;torch&lt;/code&gt; pin. Building in isolation means pip installs &lt;strong&gt;torch 2.13.0 from&lt;br&gt;
PyPI&lt;/strong&gt; — and the PyPI aarch64 wheels are built for sm_80 and up. &lt;strong&gt;No &lt;code&gt;sm_75&lt;/code&gt;.&lt;/strong&gt; Which&lt;br&gt;
destroys the entire reason for building from source on a T4G.&lt;/p&gt;

&lt;p&gt;So on this box you must build against the DLAMI's own torch, and that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python use_existing_torch.py
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-build-isolation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-build-isolation&lt;/code&gt; turns off the automatic install of everything in that &lt;code&gt;requires&lt;/code&gt;&lt;br&gt;
list.&lt;/strong&gt; From that moment on, every build dependency is yours to supply by hand — including&lt;br&gt;
&lt;code&gt;setuptools_rust&lt;/code&gt;, which is why it turns up as a bare &lt;code&gt;ModuleNotFoundError&lt;/code&gt; minutes into a&lt;br&gt;
build that has nothing visibly to do with Rust.&lt;/p&gt;

&lt;p&gt;So the toolchain was always required; isolation was just hiding it. Building this way means&lt;br&gt;
you own the dependency list, which is the rest of this walk-through. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  What the DLAMI gives you, and what it doesn't
&lt;/h4&gt;

&lt;p&gt;The AWS Deep Learning ARM64 AMI ships a &lt;strong&gt;runtime&lt;/strong&gt;, not a build environment. On a fresh box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;Present?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch 2.12 with &lt;code&gt;sm_75&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA driver&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nvcc&lt;/code&gt; / CUDA toolkit&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust toolchain&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setuptools_rust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;protoc&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of those six are on you. Let's install them.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 1 — Rust itself
&lt;/h4&gt;

&lt;p&gt;Standard rustup, nothing aarch64-specific about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;stable-aarch64-unknown-linux-gnu installed - rustc 1.97.1 (8bab26f4f 2026-07-14)

Rust is installed now. Great!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the triple: &lt;code&gt;stable-aarch64-unknown-linux-gnu&lt;/code&gt;. Rust's aarch64 support is a &lt;strong&gt;complete&lt;br&gt;
non-event&lt;/strong&gt;, which is a lovely change of pace on this hardware. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 2 — setuptools-rust
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;setuptools_rust
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Per the section above: &lt;code&gt;--no-build-isolation&lt;/code&gt; means pip won't do this for you. Do it &lt;strong&gt;early&lt;/strong&gt;&lt;br&gt;
— the failure lands during metadata generation, minutes into a build, as a bare&lt;br&gt;
&lt;code&gt;ModuleNotFoundError: No module named 'setuptools_rust'&lt;/code&gt; nowhere near anything that looks&lt;br&gt;
like Rust.&lt;/p&gt;

&lt;p&gt;⚠️ Install it into the &lt;strong&gt;same interpreter you'll build with&lt;/strong&gt;. On the DLAMI that's&lt;br&gt;
&lt;code&gt;/opt/pytorch/bin/python3&lt;/code&gt;, not the system &lt;code&gt;python3&lt;/code&gt; — they're different, and the one that&lt;br&gt;
matters is whichever owns the torch you're building against.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 3 — protoc 🔎
&lt;/h4&gt;

&lt;p&gt;This is the one nobody documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; protobuf-compiler
protoc &lt;span class="nt"&gt;--version&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;libprotoc 3.21.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code&gt;vllm-rs&lt;/code&gt; depends on the &lt;code&gt;vllm-server&lt;/code&gt; crate, &lt;code&gt;vllm-server&lt;/code&gt; builds gRPC stubs with&lt;br&gt;
&lt;code&gt;tonic&lt;/code&gt;/&lt;code&gt;prost&lt;/code&gt;, and &lt;code&gt;prost-build&lt;/code&gt; shells out to &lt;code&gt;protoc&lt;/code&gt;. Skip it and the frontend binary&lt;br&gt;
does not get built — see the summary at the end for how loudly that &lt;em&gt;doesn't&lt;/em&gt; fail.&lt;/p&gt;

&lt;p&gt;The tool parser has no protobuf dependency, which is why it builds either way.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 4 — the CUDA toolkit, while you're here
&lt;/h4&gt;

&lt;p&gt;Not Rust, but the same class of problem, and you need it for vLLM's kernels:&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;# NVIDIA's **sbsa** repo — not the x86 one, easy reflex to get wrong on Arm&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; cuda-toolkit-13-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — build the Rust artifacts
&lt;/h4&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; /opt/vllm-src
python tools/build_rust.py &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Do not omit &lt;code&gt;--release&lt;/code&gt;.&lt;/strong&gt; setuptools-rust builds inplace targets in debug by default,&lt;br&gt;
and &lt;code&gt;pip install -e .&lt;/code&gt; is an inplace build. The difference is not subtle:&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;Debug&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_rust_tool_parser.abi3.so&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100,913,216 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,009,080 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;100x.&lt;/strong&gt; The debug artifact is four times the size of &lt;em&gt;every CUDA kernel in vLLM combined&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Timing on a &lt;code&gt;g5g.xlarge&lt;/code&gt; (4 vCPU), cold:&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;real    9m1.746s
user    25m9.199s
sys     1m35.023s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;501 crates. Zero warnings. Exit 0.&lt;/strong&gt; 🟢&lt;/p&gt;

&lt;p&gt;Rust's aarch64 support does not put up a fight here — which is a pleasant contrast with the&lt;br&gt;
CUDA side of this box, where SM 7.5 on Graviton needs a custom arch list and a patched&lt;br&gt;
kernel.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 6 — check what you got
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vllm/vllm-rs vllm/_rust_tool_parser.abi3.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;-rwxr-xr-x 1 root root 50039024 vllm/vllm-rs
-rwxr-xr-x 1 root root  1009080 vllm/_rust_tool_parser.abi3.so
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file vllm/vllm-rs
&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;ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, not stripped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm/vllm-rs &lt;span class="nt"&gt;--help&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;Rust frontend and managed-engine CLI for vLLM.

Commands:
  frontend  Run the Rust OpenAI frontend as a Python-supervised worker
  serve     Launch a managed Python headless engine, then run the Rust OpenAI frontend
  bench     Run vLLM benchmarks
  render    Run engine-free request rendering and preprocessing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If &lt;code&gt;vllm/vllm-rs&lt;/code&gt; isn't there, go back to &lt;strong&gt;Step 3&lt;/strong&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 7 — run it, and mind the entrypoint ⚠️
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve google/gemma-4-E2B-it &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dtype&lt;/span&gt; float16 &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; 16384 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-num-seqs&lt;/span&gt; 8 &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;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;It must be &lt;code&gt;vllm serve&lt;/code&gt;.&lt;/strong&gt; If you launch the module directly —&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;# ❌ VLLM_USE_RUST_FRONTEND is IGNORED here&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; vllm.entrypoints.openai.api_server &lt;span class="nt"&gt;--model&lt;/span&gt; … &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— the variable does nothing. No warning, no &lt;code&gt;Unknown vLLM environment variable&lt;/code&gt; line. The&lt;br&gt;
server comes up healthy and serves happily on the Python frontend, and a benchmark run&lt;br&gt;
against it looks entirely normal.&lt;/p&gt;

&lt;p&gt;The flag is read in exactly two places:&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;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;        &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dp_supervisor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;261&lt;/span&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;api_server.py&lt;/code&gt; never mentions it.&lt;/p&gt;




&lt;h4&gt;
  
  
  How do I know it's actually Rust? 🔎
&lt;/h4&gt;

&lt;p&gt;Three checks. Do all three the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The &lt;code&gt;server:&lt;/code&gt; header:&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;curl &lt;span class="nt"&gt;-si&lt;/span&gt; localhost:8000/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^server:'&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;Frontend&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🐍 Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server: uvicorn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🦀 Rust&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(no &lt;code&gt;server:&lt;/code&gt; header at all)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. The process:&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;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; vllm-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;26588 /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17
  --input-address  ipc:///tmp/f60f3962-d45b-4bcd-9026-c0dc32736028
  --output-address ipc:///tmp/5f75411d-2787-43bb-b4fc-14bf504a1cce
  --engine-start-index 0 --engine-count 1 --data-parallel-size 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. The log prefix&lt;/strong&gt; — &lt;code&gt;(RustFrontend pid=…)&lt;/code&gt; instead of &lt;code&gt;(APIServer pid=…)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [utils.py:392] Launching Rust frontend: /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17 …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  So where does Rust actually sit?
&lt;/h4&gt;

&lt;p&gt;In &lt;strong&gt;two&lt;/strong&gt; places, and they're quite different. One is a separate process; the other is a&lt;br&gt;
shared object loaded &lt;em&gt;inside&lt;/em&gt; the Python process. Here's the whole VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ EC2 g5g.4xlarge ── Graviton2, aarch64 ─────────────────────────────────────┐
│                                                                             │
│  Deep Learning ARM64 AMI · Ubuntu 24.04 · NVIDIA driver 595.71.05           │
│  you add &amp;gt; cuda-toolkit-13-2 (sbsa) · rustup 1.97.1 · protobuf-compiler     │
│                                                                             │
│      HTTP :8000                                                             │
│          |                                                                  │
│          v                                                                  │
│  ┌───────────────────────────────┐                                          │
│  │ [RUST] vllm-rs                │  50 MB aarch64 ELF, its OWN process      │
│  │        axum 0.8.8 · tokio     │  built from the vendored rust/ workspace │
│  │        minijinja · fastokens  │  &amp;lt;- Step 5                               │
│  └────────┬─────────────▲────────┘                                          │
│           |             |                                                   │
│  ipc://   | ROUTER      | PULL     msgpack (rmp-serde / rmpv)               │
│           v             |                                                   │
│  ┌────────┴─────────────┴────────┐                                          │
│  │ [PY]   vLLM supervisor        │  `vllm serve` opens the socket, then     │
│  │                               │  hands listen-fd 17 down to vllm-rs      │
│  └────────┬──────────────────────┘                                          │
│           | spawns                                                          │
│           v                                                                 │
│  ┌───────────────────────────────┐                                          │
│  │ [PY]   EngineCore             │  torch 2.12.0+cu132, arch list has sm_75 │
│  │  ┌─────────────────────────┐  │                                          │
│  │  │ [RUST] _rust_tool_parser│  │  PyO3 .so LOADED INTO the Python         │
│  │  │        1.0 MB release   │  │  process — not a process of its own      │
│  │  └─────────────────────────┘  │                                          │
│  └────────┬──────────────────────┘                                          │
│           | CUDA                                                            │
│           v                                                                 │
│  ┌───────────────────────────────┐                                          │
│  │ NVIDIA T4G · SM 7.5           │  15,360 MiB GDDR6 · 277 GB/s measured    │
│  │ TRITON_ATTN kernels           │  weights 9.94 GiB · KV 2.95 GiB          │
│  └───────────────────────────────┘                                          │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth pulling out of that picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vllm-rs&lt;/code&gt; is not a sidecar you point at a port.&lt;/strong&gt; The Python side opens the listening
socket and passes the &lt;em&gt;file descriptor&lt;/em&gt; down. It's a worker the supervisor forks and feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_rust_tool_parser&lt;/code&gt; is Rust living inside Python.&lt;/strong&gt; It's the one that always builds
(no &lt;code&gt;protoc&lt;/code&gt; needed), which is why a broken install still leaves Rust on the box — just not
the Rust you wanted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And note where the GPU sits relative to all of this: at the bottom, behind everything. That's&lt;br&gt;
the reason the benchmark below comes out the way it does.&lt;/p&gt;


&lt;h4&gt;
  
  
  Bonus: there's a Rust benchmark client too
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_BENCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm bench serve …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same binary, &lt;code&gt;bench&lt;/code&gt; subcommand. Requires &lt;code&gt;VLLM_RUST_FRONTEND_PATH&lt;/code&gt; to resolve, so it needs&lt;br&gt;
the same Step 3 → Step 5 you just did.&lt;/p&gt;


&lt;h4&gt;
  
  
  Two warnings you should not scroll past 🔴
&lt;/h4&gt;

&lt;p&gt;The server came up healthy. These went by in the startup log anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 defeats the fast tokenizer:&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;INFO    [hf.rs:200] loading tokenizer with fastokens
WARNING [hf.rs:221] failed to load tokenizer with fastokens; falling back to
        HuggingFace tokenizers
        error=tokenizer error: normalizer error: unsupported normalizer type: Replace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fastokens&lt;/code&gt; 0.2.1 doesn't implement the &lt;code&gt;Replace&lt;/code&gt; normalizer that Gemma 4's &lt;code&gt;tokenizer.json&lt;/code&gt;&lt;br&gt;
uses, so it falls back to the same HuggingFace &lt;code&gt;tokenizers&lt;/code&gt; the Python path uses. Note the&lt;br&gt;
fallback is graceful and correct — you just don't get the fast path on &lt;em&gt;this&lt;/em&gt; model yet. It's&lt;br&gt;
a coverage gap in a young crate, and one normalizer away from closing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal isn't wired up for this model:&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;WARNING [multimodal.rs:446] multimodal model spec is not registered; disabling
        image/video support   model_id="google/gemma-4-E2B-it" model_type="gemma4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 E2B is a &lt;strong&gt;vision&lt;/strong&gt; model, and &lt;code&gt;gemma4&lt;/code&gt; isn't in the Rust multimodal spec table yet.&lt;br&gt;
Text requests behave identically and the endpoint is healthy, so nothing in a normal check&lt;br&gt;
reveals it. Also a registration gap rather than a design problem — but check it for your model&lt;br&gt;
before you switch, because a healthy endpoint won't tell you.&lt;/p&gt;




&lt;h4&gt;
  
  
  Is it faster?
&lt;/h4&gt;

&lt;p&gt;On a T4G, no. Output token throughput, same engine config, client on the box against&lt;br&gt;
localhost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.65&lt;/td&gt;
&lt;td&gt;29.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;97.48&lt;/td&gt;
&lt;td&gt;97.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;168.33&lt;/td&gt;
&lt;td&gt;169.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;169.96&lt;/td&gt;
&lt;td&gt;170.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;170.99&lt;/td&gt;
&lt;td&gt;170.34&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median TTFT tracks just as tightly — 14305 ms against 14311 ms at concurrency 32.&lt;/p&gt;

&lt;p&gt;That's the expected result, and worth saying plainly: decode on this card is&lt;br&gt;
&lt;strong&gt;bandwidth-bound&lt;/strong&gt; at a measured 277 GB/s, and the engine saturates at &lt;code&gt;--max-num-seqs 8&lt;/code&gt;.&lt;br&gt;
A frontend rewrite targets CPU-side per-request overhead. Here that overhead hides behind the&lt;br&gt;
GPU, so swapping it can't move a bottleneck-limited number. &lt;strong&gt;If you want the Rust frontend&lt;br&gt;
to buy you tokens per second on a small GPU, it won't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One signal does appear, in median inter-token latency at high concurrency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;38.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−6.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;38.41&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−5.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mean TPOT barely moves, so this is the middle of the distribution tightening rather than&lt;br&gt;
everything speeding up — the shape you'd expect from a frontend scheduling streaming work&lt;br&gt;
more evenly once many streams are in flight. Worth knowing if you serve at concurrency; not&lt;br&gt;
worth switching for on its own. 📊&lt;/p&gt;




&lt;h4&gt;
  
  
  If a plain &lt;code&gt;pip install -e .&lt;/code&gt; already ran
&lt;/h4&gt;

&lt;p&gt;A from-source vLLM install done &lt;strong&gt;without&lt;/strong&gt; the steps above succeeds, exits 0, and leaves you&lt;br&gt;
with a 96 MB debug tool parser and no frontend binary. Four defaults stack up to make that&lt;br&gt;
silent:&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;No &lt;code&gt;vllm/vllm-rs&lt;/code&gt; after a clean build&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;protoc&lt;/code&gt; absent ⇒ &lt;code&gt;vllm-server&lt;/code&gt; fails with code 101&lt;/td&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pip install&lt;/code&gt; exits 0 anyway&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;optional=not should_require_rust_frontend()&lt;/code&gt; — setuptools-rust swallows it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VLLM_REQUIRE_RUST_FRONTEND=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_rust_tool_parser.abi3.so&lt;/code&gt; is ~96 MB&lt;/td&gt;
&lt;td&gt;editable ⇒ inplace ⇒ debug profile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileNotFoundError: … vllm-rs was not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the above, discovered at import time&lt;/td&gt;
&lt;td&gt;Steps 3 + 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Healthy server, but &lt;code&gt;server: uvicorn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;flag set on the &lt;code&gt;api_server&lt;/code&gt; module, which never reads it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm serve&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;VLLM_REQUIRE_RUST_FRONTEND=1&lt;/code&gt; turns the second row into a hard build failure, which is what&lt;br&gt;
you want on any machine you plan to serve from.&lt;/p&gt;




&lt;h4&gt;
  
  
  Cheat sheet
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# toolchain — you supply these by hand because the sm_75 requirement&lt;/span&gt;
&lt;span class="c"&gt;# forces --no-build-isolation, which disables pip's automatic build deps&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
/opt/pytorch/bin/python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;setuptools_rust   &lt;span class="c"&gt;# the BUILD interpreter&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; protobuf-compiler cuda-toolkit-13-2

&lt;span class="c"&gt;# build against the DLAMI's torch, not a PyPI one (PyPI aarch64 has no sm_75)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/vllm
python use_existing_torch.py
&lt;span class="nv"&gt;TORCH_CUDA_ARCH_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7.5 &lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-build-isolation&lt;/span&gt;

&lt;span class="c"&gt;# Rust artifacts, release profile (editable installs default to debug: 100x bigger)&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python tools/build_rust.py &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="c"&gt;# confirm&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vllm/vllm-rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; vllm/vllm-rs &lt;span class="nt"&gt;--help&lt;/span&gt;

&lt;span class="c"&gt;# run — `vllm serve`, NOT the api_server module&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000

&lt;span class="c"&gt;# verify it's really Rust&lt;/span&gt;
curl &lt;span class="nt"&gt;-si&lt;/span&gt; localhost:8000/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^server:'&lt;/span&gt;   &lt;span class="c"&gt;# Rust sends none&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; vllm-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Run on EC2 &lt;code&gt;g5g.xlarge&lt;/code&gt; and &lt;code&gt;g5g.4xlarge&lt;/code&gt;, &lt;code&gt;us-east-1a&lt;/code&gt;, NVIDIA T4G (SM 7.5). vLLM&lt;br&gt;
&lt;code&gt;0.27.2rc1.dev0+g7f7a32cfe&lt;/code&gt;, rustc 1.97.1, setuptools-rust 1.13.0, libprotoc 3.21.12,&lt;br&gt;
torch 2.12.0+cu132. Benchmarks are one run per cell for Rust and two for Python; treat the&lt;br&gt;
TPOT delta as suggestive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>vllm</category>
      <category>aws</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Installing Rust for vLLM on Graviton: a G5g walk-through 🦀</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 20:37:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/installing-rust-for-vllm-on-graviton-a-g5g-walk-through-599a</link>
      <guid>https://dev.to/aws-builders/installing-rust-for-vllm-on-graviton-a-g5g-walk-through-599a</guid>
      <description>&lt;p&gt;This tutorial walks through installing and setting up the &lt;strong&gt;Rust toolchain for vLLM&lt;/strong&gt; on an&lt;br&gt;
AWS EC2 &lt;strong&gt;G5g&lt;/strong&gt; instance — Graviton2 (aarch64) with an NVIDIA T4G GPU — and getting vLLM's&lt;br&gt;
Rust frontend (&lt;code&gt;vllm-rs&lt;/code&gt;) built, running, and &lt;em&gt;verified&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This paper is a follow-on to the original G5g Gemma 4 build.&lt;/p&gt;

&lt;p&gt;Everything below was run on the box. 🦀&lt;/p&gt;


&lt;h4&gt;
  
  
  Wait, vLLM has Rust in it?
&lt;/h4&gt;

&lt;p&gt;You betcha. Since &lt;a href="https://github.com/vllm-project/vllm/pull/40848" rel="noopener noreferrer"&gt;PR #40848&lt;/a&gt; (merged&lt;br&gt;
2026-05-21), vLLM vendors a &lt;strong&gt;14-crate Rust workspace&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;bench  chat  cmd  engine-core-client  llm  managed-engine  metrics
mock-engine  parser  parser/python  server  text  tokenizer  tracing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edition 2024, resolver 3. Straight from the vendored &lt;code&gt;rust/Cargo.toml&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;Crate&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;axum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8.8&lt;/td&gt;
&lt;td&gt;the HTTP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.47.1&lt;/td&gt;
&lt;td&gt;async runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zeromq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6.0&lt;/td&gt;
&lt;td&gt;talks to the Python engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rmp-serde&lt;/code&gt; / &lt;code&gt;rmpv&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.3.1&lt;/td&gt;
&lt;td&gt;msgpack on the wire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minijinja&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.22&lt;/td&gt;
&lt;td&gt;chat templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tonic&lt;/code&gt; / &lt;code&gt;prost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.14.6 / 0.14.3&lt;/td&gt;
&lt;td&gt;gRPC — &lt;strong&gt;remember this one&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's a drop-in replacement for the Python FastAPI server. Two artifacts get built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🦀 &lt;strong&gt;&lt;code&gt;vllm-rs&lt;/code&gt;&lt;/strong&gt; — the axum frontend binary&lt;/li&gt;
&lt;li&gt;🐍 &lt;strong&gt;&lt;code&gt;vllm._rust_tool_parser&lt;/code&gt;&lt;/strong&gt; — a PyO3 extension module&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Rust is a build requirement now
&lt;/h4&gt;

&lt;p&gt;That's the headline, and it's reason enough on its own: &lt;strong&gt;you cannot build vLLM from source at&lt;br&gt;
v0.27.2rc0 without Rust in the picture.&lt;/strong&gt; &lt;code&gt;setup.py&lt;/code&gt; imports it at module scope, line 21,&lt;br&gt;
unguarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;setuptools_rust.build&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build_rust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;try&lt;/code&gt;, no feature flag, no opt-out. Metadata generation doesn't happen without it.&lt;/p&gt;

&lt;p&gt;And this isn't a quirk of one release. vLLM's Rust surface is &lt;strong&gt;14 crates&lt;/strong&gt; covering the HTTP&lt;br&gt;
frontend, the tool parser, the tokenizer and the benchmark client, and it has been growing&lt;br&gt;
since it landed. If you build inference infrastructure from source, a Rust toolchain is&lt;br&gt;
becoming table stakes — so it's worth knowing how to drive it properly rather than working&lt;br&gt;
around it.&lt;/p&gt;

&lt;p&gt;Three things do get conflated, though, and they have different scopes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Needed to build vLLM?&lt;/th&gt;
&lt;th&gt;Needed to serve?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;setuptools_rust&lt;/code&gt; (Python pkg)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes, always&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cargo&lt;/code&gt; / &lt;code&gt;rustc&lt;/code&gt; toolchain&lt;/td&gt;
&lt;td&gt;for working Rust artifacts&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;protoc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;for &lt;code&gt;vllm-rs&lt;/code&gt; specifically&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Then why doesn't &lt;code&gt;pip install vllm&lt;/code&gt; need this?
&lt;/h4&gt;

&lt;p&gt;Because normally pip installs it for you. &lt;code&gt;pyproject.toml&lt;/code&gt; declares it:&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;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"cmake&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.26&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;ninja&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;packaging&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;24.2&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;77.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;81.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;setuptools-scm&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools-rust&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",          # &amp;lt;- pip grabs this automatically&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"torch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.13&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",                 # &amp;lt;- ...and this. Which is the problem.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="s"&gt;"wheel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jinja2"&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;Under normal &lt;strong&gt;build isolation&lt;/strong&gt;, pip creates a clean env, installs that list, and builds.&lt;br&gt;
You never see &lt;code&gt;setuptools_rust&lt;/code&gt; because you never had to think about it.&lt;/p&gt;

&lt;p&gt;But look at the &lt;code&gt;torch&lt;/code&gt; pin. Building in isolation means pip installs &lt;strong&gt;torch 2.13.0 from&lt;br&gt;
PyPI&lt;/strong&gt; — and the PyPI aarch64 wheels are built for sm_80 and up. &lt;strong&gt;No &lt;code&gt;sm_75&lt;/code&gt;.&lt;/strong&gt; Which&lt;br&gt;
destroys the entire reason for building from source on a T4G.&lt;/p&gt;

&lt;p&gt;So on this box you must build against the DLAMI's own torch, and that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python use_existing_torch.py
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-build-isolation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-build-isolation&lt;/code&gt; turns off the automatic install of everything in that &lt;code&gt;requires&lt;/code&gt;&lt;br&gt;
list.&lt;/strong&gt; From that moment on, every build dependency is yours to supply by hand — including&lt;br&gt;
&lt;code&gt;setuptools_rust&lt;/code&gt;, which is why it turns up as a bare &lt;code&gt;ModuleNotFoundError&lt;/code&gt; minutes into a&lt;br&gt;
build that has nothing visibly to do with Rust.&lt;/p&gt;

&lt;p&gt;So the toolchain was always required; isolation was just hiding it. Building this way means&lt;br&gt;
you own the dependency list, which is the rest of this walk-through. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  What the DLAMI gives you, and what it doesn't
&lt;/h4&gt;

&lt;p&gt;The AWS Deep Learning ARM64 AMI ships a &lt;strong&gt;runtime&lt;/strong&gt;, not a build environment. On a fresh box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;Present?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch 2.12 with &lt;code&gt;sm_75&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA driver&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nvcc&lt;/code&gt; / CUDA toolkit&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust toolchain&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setuptools_rust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;protoc&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of those six are on you. Let's install them.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 1 — Rust itself
&lt;/h4&gt;

&lt;p&gt;Standard rustup, nothing aarch64-specific about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;stable-aarch64-unknown-linux-gnu installed - rustc 1.97.1 (8bab26f4f 2026-07-14)

Rust is installed now. Great!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the triple: &lt;code&gt;stable-aarch64-unknown-linux-gnu&lt;/code&gt;. Rust's aarch64 support is a &lt;strong&gt;complete&lt;br&gt;
non-event&lt;/strong&gt;, which is a lovely change of pace on this hardware. ⚡&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 2 — setuptools-rust
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;setuptools_rust
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Per the section above: &lt;code&gt;--no-build-isolation&lt;/code&gt; means pip won't do this for you. Do it &lt;strong&gt;early&lt;/strong&gt;&lt;br&gt;
— the failure lands during metadata generation, minutes into a build, as a bare&lt;br&gt;
&lt;code&gt;ModuleNotFoundError: No module named 'setuptools_rust'&lt;/code&gt; nowhere near anything that looks&lt;br&gt;
like Rust.&lt;/p&gt;

&lt;p&gt;⚠️ Install it into the &lt;strong&gt;same interpreter you'll build with&lt;/strong&gt;. On the DLAMI that's&lt;br&gt;
&lt;code&gt;/opt/pytorch/bin/python3&lt;/code&gt;, not the system &lt;code&gt;python3&lt;/code&gt; — they're different, and the one that&lt;br&gt;
matters is whichever owns the torch you're building against.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 3 — protoc 🔎
&lt;/h4&gt;

&lt;p&gt;This is the one nobody documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; protobuf-compiler
protoc &lt;span class="nt"&gt;--version&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;libprotoc 3.21.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code&gt;vllm-rs&lt;/code&gt; depends on the &lt;code&gt;vllm-server&lt;/code&gt; crate, &lt;code&gt;vllm-server&lt;/code&gt; builds gRPC stubs with&lt;br&gt;
&lt;code&gt;tonic&lt;/code&gt;/&lt;code&gt;prost&lt;/code&gt;, and &lt;code&gt;prost-build&lt;/code&gt; shells out to &lt;code&gt;protoc&lt;/code&gt;. Skip it and the frontend binary&lt;br&gt;
does not get built — see the summary at the end for how loudly that &lt;em&gt;doesn't&lt;/em&gt; fail.&lt;/p&gt;

&lt;p&gt;The tool parser has no protobuf dependency, which is why it builds either way.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 4 — the CUDA toolkit, while you're here
&lt;/h4&gt;

&lt;p&gt;Not Rust, but the same class of problem, and you need it for vLLM's kernels:&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;# NVIDIA's **sbsa** repo — not the x86 one, easy reflex to get wrong on Arm&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; cuda-toolkit-13-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — build the Rust artifacts
&lt;/h4&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; /opt/vllm-src
python tools/build_rust.py &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Do not omit &lt;code&gt;--release&lt;/code&gt;.&lt;/strong&gt; setuptools-rust builds inplace targets in debug by default,&lt;br&gt;
and &lt;code&gt;pip install -e .&lt;/code&gt; is an inplace build. The difference is not subtle:&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;Debug&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_rust_tool_parser.abi3.so&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100,913,216 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,009,080 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;100x.&lt;/strong&gt; The debug artifact is four times the size of &lt;em&gt;every CUDA kernel in vLLM combined&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Timing on a &lt;code&gt;g5g.xlarge&lt;/code&gt; (4 vCPU), cold:&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;real    9m1.746s
user    25m9.199s
sys     1m35.023s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;501 crates. Zero warnings. Exit 0.&lt;/strong&gt; 🟢&lt;/p&gt;

&lt;p&gt;Rust's aarch64 support does not put up a fight here — which is a pleasant contrast with the&lt;br&gt;
CUDA side of this box, where SM 7.5 on Graviton needs a custom arch list and a patched&lt;br&gt;
kernel.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 6 — check what you got
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vllm/vllm-rs vllm/_rust_tool_parser.abi3.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;-rwxr-xr-x 1 root root 50039024 vllm/vllm-rs
-rwxr-xr-x 1 root root  1009080 vllm/_rust_tool_parser.abi3.so
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file vllm/vllm-rs
&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;ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, not stripped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm/vllm-rs &lt;span class="nt"&gt;--help&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;Rust frontend and managed-engine CLI for vLLM.

Commands:
  frontend  Run the Rust OpenAI frontend as a Python-supervised worker
  serve     Launch a managed Python headless engine, then run the Rust OpenAI frontend
  bench     Run vLLM benchmarks
  render    Run engine-free request rendering and preprocessing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If &lt;code&gt;vllm/vllm-rs&lt;/code&gt; isn't there, go back to &lt;strong&gt;Step 3&lt;/strong&gt;.&lt;/p&gt;


&lt;h4&gt;
  
  
  Step 7 — run it, and mind the entrypoint ⚠️
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve google/gemma-4-E2B-it &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dtype&lt;/span&gt; float16 &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; 16384 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-num-seqs&lt;/span&gt; 8 &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;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;It must be &lt;code&gt;vllm serve&lt;/code&gt;.&lt;/strong&gt; If you launch the module directly —&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;# ❌ VLLM_USE_RUST_FRONTEND is IGNORED here&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; vllm.entrypoints.openai.api_server &lt;span class="nt"&gt;--model&lt;/span&gt; … &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— the variable does nothing. No warning, no &lt;code&gt;Unknown vLLM environment variable&lt;/code&gt; line. The&lt;br&gt;
server comes up healthy and serves happily on the Python frontend, and a benchmark run&lt;br&gt;
against it looks entirely normal.&lt;/p&gt;

&lt;p&gt;The flag is read in exactly two places:&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;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;        &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dp_supervisor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;261&lt;/span&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;api_server.py&lt;/code&gt; never mentions it.&lt;/p&gt;




&lt;h4&gt;
  
  
  How do I know it's actually Rust? 🔎
&lt;/h4&gt;

&lt;p&gt;Three checks. Do all three the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The &lt;code&gt;server:&lt;/code&gt; header:&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;curl &lt;span class="nt"&gt;-si&lt;/span&gt; localhost:8000/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^server:'&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;Frontend&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🐍 Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server: uvicorn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🦀 Rust&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(no &lt;code&gt;server:&lt;/code&gt; header at all)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. The process:&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;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; vllm-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;26588 /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17
  --input-address  ipc:///tmp/f60f3962-d45b-4bcd-9026-c0dc32736028
  --output-address ipc:///tmp/5f75411d-2787-43bb-b4fc-14bf504a1cce
  --engine-start-index 0 --engine-count 1 --data-parallel-size 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. The log prefix&lt;/strong&gt; — &lt;code&gt;(RustFrontend pid=…)&lt;/code&gt; instead of &lt;code&gt;(APIServer pid=…)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [utils.py:392] Launching Rust frontend: /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17 …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  So where does Rust actually sit?
&lt;/h4&gt;

&lt;p&gt;In &lt;strong&gt;two&lt;/strong&gt; places, and they're quite different. One is a separate process; the other is a&lt;br&gt;
shared object loaded &lt;em&gt;inside&lt;/em&gt; the Python process. Here's the whole VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ EC2 g5g.4xlarge ── Graviton2, aarch64 ─────────────────────────────────────┐
│                                                                             │
│  Deep Learning ARM64 AMI · Ubuntu 24.04 · NVIDIA driver 595.71.05           │
│  you add &amp;gt; cuda-toolkit-13-2 (sbsa) · rustup 1.97.1 · protobuf-compiler     │
│                                                                             │
│      HTTP :8000                                                             │
│          |                                                                  │
│          v                                                                  │
│  ┌───────────────────────────────┐                                          │
│  │ [RUST] vllm-rs                │  50 MB aarch64 ELF, its OWN process      │
│  │        axum 0.8.8 · tokio     │  built from the vendored rust/ workspace │
│  │        minijinja · fastokens  │  &amp;lt;- Step 5                               │
│  └────────┬─────────────▲────────┘                                          │
│           |             |                                                   │
│  ipc://   | ROUTER      | PULL     msgpack (rmp-serde / rmpv)               │
│           v             |                                                   │
│  ┌────────┴─────────────┴────────┐                                          │
│  │ [PY]   vLLM supervisor        │  `vllm serve` opens the socket, then     │
│  │                               │  hands listen-fd 17 down to vllm-rs      │
│  └────────┬──────────────────────┘                                          │
│           | spawns                                                          │
│           v                                                                 │
│  ┌───────────────────────────────┐                                          │
│  │ [PY]   EngineCore             │  torch 2.12.0+cu132, arch list has sm_75 │
│  │  ┌─────────────────────────┐  │                                          │
│  │  │ [RUST] _rust_tool_parser│  │  PyO3 .so LOADED INTO the Python         │
│  │  │        1.0 MB release   │  │  process — not a process of its own      │
│  │  └─────────────────────────┘  │                                          │
│  └────────┬──────────────────────┘                                          │
│           | CUDA                                                            │
│           v                                                                 │
│  ┌───────────────────────────────┐                                          │
│  │ NVIDIA T4G · SM 7.5           │  15,360 MiB GDDR6 · 277 GB/s measured    │
│  │ TRITON_ATTN kernels           │  weights 9.94 GiB · KV 2.95 GiB          │
│  └───────────────────────────────┘                                          │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth pulling out of that picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vllm-rs&lt;/code&gt; is not a sidecar you point at a port.&lt;/strong&gt; The Python side opens the listening
socket and passes the &lt;em&gt;file descriptor&lt;/em&gt; down. It's a worker the supervisor forks and feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_rust_tool_parser&lt;/code&gt; is Rust living inside Python.&lt;/strong&gt; It's the one that always builds
(no &lt;code&gt;protoc&lt;/code&gt; needed), which is why a broken install still leaves Rust on the box — just not
the Rust you wanted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And note where the GPU sits relative to all of this: at the bottom, behind everything. That's&lt;br&gt;
the reason the benchmark below comes out the way it does.&lt;/p&gt;


&lt;h4&gt;
  
  
  Bonus: there's a Rust benchmark client too
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_BENCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm bench serve …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same binary, &lt;code&gt;bench&lt;/code&gt; subcommand. Requires &lt;code&gt;VLLM_RUST_FRONTEND_PATH&lt;/code&gt; to resolve, so it needs&lt;br&gt;
the same Step 3 → Step 5 you just did.&lt;/p&gt;


&lt;h4&gt;
  
  
  Two warnings you should not scroll past 🔴
&lt;/h4&gt;

&lt;p&gt;The server came up healthy. These went by in the startup log anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 defeats the fast tokenizer:&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;INFO    [hf.rs:200] loading tokenizer with fastokens
WARNING [hf.rs:221] failed to load tokenizer with fastokens; falling back to
        HuggingFace tokenizers
        error=tokenizer error: normalizer error: unsupported normalizer type: Replace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fastokens&lt;/code&gt; 0.2.1 doesn't implement the &lt;code&gt;Replace&lt;/code&gt; normalizer that Gemma 4's &lt;code&gt;tokenizer.json&lt;/code&gt;&lt;br&gt;
uses, so it falls back to the same HuggingFace &lt;code&gt;tokenizers&lt;/code&gt; the Python path uses. Note the&lt;br&gt;
fallback is graceful and correct — you just don't get the fast path on &lt;em&gt;this&lt;/em&gt; model yet. It's&lt;br&gt;
a coverage gap in a young crate, and one normalizer away from closing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal isn't wired up for this model:&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;WARNING [multimodal.rs:446] multimodal model spec is not registered; disabling
        image/video support   model_id="google/gemma-4-E2B-it" model_type="gemma4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 E2B is a &lt;strong&gt;vision&lt;/strong&gt; model, and &lt;code&gt;gemma4&lt;/code&gt; isn't in the Rust multimodal spec table yet.&lt;br&gt;
Text requests behave identically and the endpoint is healthy, so nothing in a normal check&lt;br&gt;
reveals it. Also a registration gap rather than a design problem — but check it for your model&lt;br&gt;
before you switch, because a healthy endpoint won't tell you.&lt;/p&gt;




&lt;h4&gt;
  
  
  Is it faster?
&lt;/h4&gt;

&lt;p&gt;On a T4G, no. Output token throughput, same engine config, client on the box against&lt;br&gt;
localhost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.65&lt;/td&gt;
&lt;td&gt;29.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;97.48&lt;/td&gt;
&lt;td&gt;97.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;168.33&lt;/td&gt;
&lt;td&gt;169.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;169.96&lt;/td&gt;
&lt;td&gt;170.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;170.99&lt;/td&gt;
&lt;td&gt;170.34&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median TTFT tracks just as tightly — 14305 ms against 14311 ms at concurrency 32.&lt;/p&gt;

&lt;p&gt;That's the expected result, and worth saying plainly: decode on this card is&lt;br&gt;
&lt;strong&gt;bandwidth-bound&lt;/strong&gt; at a measured 277 GB/s, and the engine saturates at &lt;code&gt;--max-num-seqs 8&lt;/code&gt;.&lt;br&gt;
A frontend rewrite targets CPU-side per-request overhead. Here that overhead hides behind the&lt;br&gt;
GPU, so swapping it can't move a bottleneck-limited number. &lt;strong&gt;If you want the Rust frontend&lt;br&gt;
to buy you tokens per second on a small GPU, it won't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One signal does appear, in median inter-token latency at high concurrency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;🐍 Python&lt;/th&gt;
&lt;th&gt;🦀 Rust&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;38.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−6.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;38.41&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−5.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mean TPOT barely moves, so this is the middle of the distribution tightening rather than&lt;br&gt;
everything speeding up — the shape you'd expect from a frontend scheduling streaming work&lt;br&gt;
more evenly once many streams are in flight. Worth knowing if you serve at concurrency; not&lt;br&gt;
worth switching for on its own. 📊&lt;/p&gt;




&lt;h4&gt;
  
  
  If a plain &lt;code&gt;pip install -e .&lt;/code&gt; already ran
&lt;/h4&gt;

&lt;p&gt;A from-source vLLM install done &lt;strong&gt;without&lt;/strong&gt; the steps above succeeds, exits 0, and leaves you&lt;br&gt;
with a 96 MB debug tool parser and no frontend binary. Four defaults stack up to make that&lt;br&gt;
silent:&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;No &lt;code&gt;vllm/vllm-rs&lt;/code&gt; after a clean build&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;protoc&lt;/code&gt; absent ⇒ &lt;code&gt;vllm-server&lt;/code&gt; fails with code 101&lt;/td&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pip install&lt;/code&gt; exits 0 anyway&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;optional=not should_require_rust_frontend()&lt;/code&gt; — setuptools-rust swallows it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VLLM_REQUIRE_RUST_FRONTEND=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_rust_tool_parser.abi3.so&lt;/code&gt; is ~96 MB&lt;/td&gt;
&lt;td&gt;editable ⇒ inplace ⇒ debug profile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileNotFoundError: … vllm-rs was not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the above, discovered at import time&lt;/td&gt;
&lt;td&gt;Steps 3 + 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Healthy server, but &lt;code&gt;server: uvicorn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;flag set on the &lt;code&gt;api_server&lt;/code&gt; module, which never reads it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm serve&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;VLLM_REQUIRE_RUST_FRONTEND=1&lt;/code&gt; turns the second row into a hard build failure, which is what&lt;br&gt;
you want on any machine you plan to serve from.&lt;/p&gt;




&lt;h4&gt;
  
  
  Cheat sheet
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# toolchain — you supply these by hand because the sm_75 requirement&lt;/span&gt;
&lt;span class="c"&gt;# forces --no-build-isolation, which disables pip's automatic build deps&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
/opt/pytorch/bin/python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;setuptools_rust   &lt;span class="c"&gt;# the BUILD interpreter&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; protobuf-compiler cuda-toolkit-13-2

&lt;span class="c"&gt;# build against the DLAMI's torch, not a PyPI one (PyPI aarch64 has no sm_75)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/vllm
python use_existing_torch.py
&lt;span class="nv"&gt;TORCH_CUDA_ARCH_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7.5 &lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-build-isolation&lt;/span&gt;

&lt;span class="c"&gt;# Rust artifacts, release profile (editable installs default to debug: 100x bigger)&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python tools/build_rust.py &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="c"&gt;# confirm&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vllm/vllm-rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; vllm/vllm-rs &lt;span class="nt"&gt;--help&lt;/span&gt;

&lt;span class="c"&gt;# run — `vllm serve`, NOT the api_server module&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000

&lt;span class="c"&gt;# verify it's really Rust&lt;/span&gt;
curl &lt;span class="nt"&gt;-si&lt;/span&gt; localhost:8000/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^server:'&lt;/span&gt;   &lt;span class="c"&gt;# Rust sends none&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; vllm-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Run on EC2 &lt;code&gt;g5g.xlarge&lt;/code&gt; and &lt;code&gt;g5g.4xlarge&lt;/code&gt;, &lt;code&gt;us-east-1a&lt;/code&gt;, NVIDIA T4G (SM 7.5). vLLM&lt;br&gt;
&lt;code&gt;0.27.2rc1.dev0+g7f7a32cfe&lt;/code&gt;, rustc 1.97.1, setuptools-rust 1.13.0, libprotoc 3.21.12,&lt;br&gt;
torch 2.12.0+cu132. Benchmarks are one run per cell for Rust and two for Python; treat the&lt;br&gt;
TPOT delta as suggestive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>vllm</category>
      <category>aws</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Building and Serving vLLM with Rust</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 20:12:39 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-and-serving-vllm-with-rust-7c8</link>
      <guid>https://dev.to/aws-builders/building-and-serving-vllm-with-rust-7c8</guid>
      <description>&lt;p&gt;This tutorial walks through deploying vLLM and some of the key Rust tools used for building and deployment.&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%2F94odygp6vezrwv8n5jr7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94odygp6vezrwv8n5jr7.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tutorial walks through installing and setting up the &lt;strong&gt;Rust toolchain for vLLM&lt;/strong&gt; on an AWS EC2 &lt;strong&gt;G5g&lt;/strong&gt; instance — Graviton2 (aarch64) with an NVIDIA T4G GPU — and getting vLLM’s Rust frontend (vllm-rs) built, running, and &lt;em&gt;verified&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you build vLLM from source, some of this applies to you on any architecture: since v0.27.2rc0 the build has a hard Rust dependency. The aarch64 + Turing box is just where every sharp edge shows up at once.&lt;/p&gt;

&lt;p&gt;Everything below was run on the box. 🦀&lt;/p&gt;

&lt;h4&gt;
  
  
  Wait, vLLM has Rust in it?
&lt;/h4&gt;

&lt;p&gt;You betcha. Since &lt;a href="https://github.com/vllm-project/vllm/pull/40848" rel="noopener noreferrer"&gt;PR #40848&lt;/a&gt; (merged 2026–05–21), vLLM vendors a &lt;strong&gt;14-crate Rust workspace&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;bench chat cmd engine-core-client llm managed-engine metrics
mock-engine parser parser/python server text tokenizer tracing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edition 2024, resolver 3. Straight from the vendored rust/Cargo.toml:&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%2Feg10mr3mjumt19pjh1z6.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%2Feg10mr3mjumt19pjh1z6.png" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a drop-in replacement for the Python FastAPI server. Two artifacts get built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🦀 &lt;strong&gt;vllm-rs&lt;/strong&gt;  — the axum frontend binary&lt;/li&gt;
&lt;li&gt;🐍 &lt;strong&gt;vllm._rust_tool_parser&lt;/strong&gt;  — a PyO3 extension module&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Rust is a build requirement now
&lt;/h4&gt;

&lt;p&gt;That’s the headline, and it’s reason enough on its own: &lt;strong&gt;you cannot build vLLM from source at v0.27.2rc0 without Rust in the picture.&lt;/strong&gt; setup.py imports it at module scope, line 21, unguarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;setuptools_rust.build&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build_rust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No try, no feature flag, no opt-out. Metadata generation doesn't happen without it.&lt;/p&gt;

&lt;p&gt;And this isn’t a quirk of one release. vLLM’s Rust surface is &lt;strong&gt;14 crates&lt;/strong&gt; covering the HTTP frontend, the tool parser, the tokenizer and the benchmark client, and it has been growing since it landed. If you build inference infrastructure from source, a Rust toolchain is becoming table stakes — so it’s worth knowing how to drive it properly rather than working around it.&lt;/p&gt;

&lt;p&gt;Three things do get conflated, though, and they have different scopes:&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%2Fcngr3al1jlqjtkxozk8c.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%2Fcngr3al1jlqjtkxozk8c.png" width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Then why doesn’t pip install vllm need this?
&lt;/h4&gt;

&lt;p&gt;Because normally pip installs it for you. pyproject.toml declares it:&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;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"cmake&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.26&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;ninja&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;packaging&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;24.2&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;77.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;81.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;setuptools-scm&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"setuptools-rust&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;", # &amp;lt;- pip grabs this automatically&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"torch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.13&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;", # &amp;lt;- ...and this. Which is the problem.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="s"&gt;"wheel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jinja2"&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;Under normal &lt;strong&gt;build isolation&lt;/strong&gt; , pip creates a clean env, installs that list, and builds. You never see setuptools_rust because you never had to think about it.&lt;/p&gt;

&lt;p&gt;But look at the torch pin. Building in isolation means pip installs &lt;strong&gt;torch 2.13.0 from PyPI&lt;/strong&gt;  — and the PyPI aarch64 wheels are built for sm_80 and up. &lt;strong&gt;No&lt;/strong&gt;  &lt;strong&gt;sm_75.&lt;/strong&gt; Which destroys the entire reason for building from source on a T4G.&lt;/p&gt;

&lt;p&gt;So on this box you must build against the DLAMI’s own torch, and that means:&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 use_existing_torch.py
pip install -e . --no-build-isolation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;--no-build-isolation turns off the automatic install of everything in that&lt;/strong&gt;  &lt;strong&gt;requires list.&lt;/strong&gt; From that moment on, every build dependency is yours to supply by hand — including setuptools_rust, which is why it turns up as a bare ModuleNotFoundError minutes into a build that has nothing visibly to do with Rust.&lt;/p&gt;

&lt;p&gt;So the toolchain was always required; isolation was just hiding it. Building this way means you own the dependency list, which is the rest of this walk-through. ⚡&lt;/p&gt;

&lt;h4&gt;
  
  
  What the DLAMI gives you, and what it doesn’t
&lt;/h4&gt;

&lt;p&gt;The AWS Deep Learning ARM64 AMI ships a &lt;strong&gt;runtime&lt;/strong&gt; , not a build environment. On a fresh box:&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%2F3edz50dbpwk92jx7b6er.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%2F3edz50dbpwk92jx7b6er.png" width="663" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four of those six are on you. Let’s install them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1 — Rust itself
&lt;/h4&gt;

&lt;p&gt;Standard rustup, nothing aarch64-specific about it:&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;curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
&lt;/span&gt;&lt;span class="gp"&gt;. "$&lt;/span&gt;HOME/.cargo/env&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="go"&gt;
stable-aarch64-unknown-linux-gnu installed - rustc 1.97.1 (8bab26f4f 2026-07-14)

Rust is installed now. Great!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the triple: stable-aarch64-unknown-linux-gnu. Rust's aarch64 support is a &lt;strong&gt;complete non-event&lt;/strong&gt; , which is a lovely change of pace on this hardware. ⚡&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2 — setuptools-rust
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python3 -m pip install setuptools_rust
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the section above: --no-build-isolation means pip won't do this for you. Do it &lt;strong&gt;early&lt;/strong&gt;  — the failure lands during metadata generation, minutes into a build, as a bare ModuleNotFoundError: No module named 'setuptools_rust' nowhere near anything that looks like Rust.&lt;/p&gt;

&lt;p&gt;⚠️ Install it into the &lt;strong&gt;same interpreter you’ll build with&lt;/strong&gt;. On the DLAMI that’s /opt/pytorch/bin/python3, not the system python3 — they're different, and the one that matters is whichever owns the torch you're building against.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3 — protoc 🔎
&lt;/h4&gt;

&lt;p&gt;This is the one nobody documents:&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;apt-get install -y protobuf-compiler
protoc --version

libprotoc 3.21.12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; vllm-rs depends on the vllm-server crate, vllm-server builds gRPC stubs with tonic/prost, and prost-build shells out to protoc. Skip it and the frontend binary does not get built — see the summary at the end for how loudly that &lt;em&gt;doesn't&lt;/em&gt; fail.&lt;/p&gt;

&lt;p&gt;The tool parser has no protobuf dependency, which is why it builds either way.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4 — the CUDA toolkit, while you’re here
&lt;/h4&gt;

&lt;p&gt;Not Rust, but the same class of problem, and you need it for vLLM’s kernels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# NVIDIA's **sbsa** repo — not the x86 one, easy reflex to get wrong on Arm&lt;/span&gt;
apt-get install -y cuda-toolkit-13-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 5 — build the Rust artifacts
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;cd /opt/vllm-src
python tools/build_rust.py --release
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Do not omit&lt;/strong&gt;  &lt;strong&gt;--release.&lt;/strong&gt; setuptools-rust builds inplace targets in debug by default, and pip install -e . is an inplace build. The difference is not subtle:&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%2Fm4qq2bjqni8nfrzm8t80.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%2Fm4qq2bjqni8nfrzm8t80.png" width="800" height="115"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100x.&lt;/strong&gt; The debug artifact is four times the size of &lt;em&gt;every CUDA kernel in vLLM combined&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Timing on a g5g.xlarge (4 vCPU), cold:&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;real 9m1.746s
user 25m9.199s
sys 1m35.023s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;501 crates. Zero warnings. Exit 0.&lt;/strong&gt;  🟢&lt;/p&gt;

&lt;p&gt;Rust’s aarch64 support does not put up a fight here — which is a pleasant contrast with the CUDA side of this box, where SM 7.5 on Graviton needs a custom arch list and a patched kernel.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 6 — check what you got
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ls -la vllm/vllm-rs vllm/_rust_tool_parser.abi3.so

-rwxr-xr-x 1 root root 50039024 vllm/vllm-rs
-rwxr-xr-x 1 root root 1009080 vllm/_rust_tool_parser.abi3.so

file vllm/vllm-rs

ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, not stripped

vllm/vllm-rs --help

Rust frontend and managed-engine CLI for vLLM.

Commands:
  frontend Run the Rust OpenAI frontend as a Python-supervised worker
  serve Launch a managed Python headless engine, then run the Rust OpenAI frontend
  bench Run vLLM benchmarks
  render Run engine-free request rendering and preprocessing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If vllm/vllm-rs isn't there, go back to &lt;strong&gt;Step 3&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 7 — run it, and mind the entrypoint ⚠️
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve google/gemma-4-E2B-it &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dtype&lt;/span&gt; float16 &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; 16384 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-num-seqs&lt;/span&gt; 8 &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;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It must be&lt;/strong&gt;  &lt;strong&gt;vllm serve.&lt;/strong&gt; If you launch the module directly —&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;# ❌ VLLM_USE_RUST_FRONTEND is IGNORED here&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; vllm.entrypoints.openai.api_server &lt;span class="nt"&gt;--model&lt;/span&gt; … &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— the variable does nothing. No warning, no Unknown vLLM environment variable line. The server comes up healthy and serves happily on the Python frontend, and a benchmark run against it looks entirely normal.&lt;/p&gt;

&lt;p&gt;The flag is read in exactly two places:&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;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;entrypoints&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dp_supervisor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;261&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VLLM_RUST_FRONTEND_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;api_server.py never mentions it.&lt;/p&gt;

&lt;h4&gt;
  
  
  How do I know it’s actually Rust? 🔎
&lt;/h4&gt;

&lt;p&gt;Three checks. Do all three the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The&lt;/strong&gt;  &lt;strong&gt;server: header:&lt;/strong&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;curl -si localhost:8000/health | grep -i '^server:'
&lt;/span&gt;&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%2Fd5pn7edx4gnwcuc6vdlz.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%2Fd5pn7edx4gnwcuc6vdlz.png" width="679" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The process:&lt;/strong&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;pgrep -af vllm-rs

26588 /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17
  --input-address ipc:///tmp/f60f3962-d45b-4bcd-9026-c0dc32736028
  --output-address ipc:///tmp/5f75411d-2787-43bb-b4fc-14bf504a1cce
  --engine-start-index 0 --engine-count 1 --data-parallel-size 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. The log prefix&lt;/strong&gt;  — (RustFrontend pid=…) instead of (APIServer pid=…):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [utils.py:392] Launching Rust frontend: /opt/vllm-src/vllm/vllm-rs frontend --listen-fd 17 …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  So where does Rust actually sit?
&lt;/h4&gt;

&lt;p&gt;In &lt;strong&gt;two&lt;/strong&gt; places, and they’re quite different. One is a separate process; the other is a shared object loaded &lt;em&gt;inside&lt;/em&gt; the Python process. Here’s the whole VM:&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%2F2m396u24ydkv3u8v4gyd.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%2F2m396u24ydkv3u8v4gyd.png" width="771" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things worth pulling out of that picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vllm-rs is not a sidecar you point at a port.&lt;/strong&gt; The Python side opens the listening socket and passes the &lt;em&gt;file descriptor&lt;/em&gt; down. It's a worker the supervisor forks and feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;_rust_tool_parser is Rust living inside Python.&lt;/strong&gt; It's the one that always builds (no protoc needed), which is why a broken install still leaves Rust on the box — just not the Rust you wanted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And note where the GPU sits relative to all of this: at the bottom, behind everything. That’s the reason the benchmark below comes out the way it does.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bonus: there’s a Rust benchmark client too
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VLLM_USE_RUST_BENCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm bench serve …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same binary, bench subcommand. Requires VLLM_RUST_FRONTEND_PATH to resolve, so it needs the same Step 3 → Step 5 you just did.&lt;/p&gt;

&lt;h4&gt;
  
  
  Two warnings you should not scroll past 🔴
&lt;/h4&gt;

&lt;p&gt;The server came up healthy. These went by in the startup log anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 defeats the fast tokenizer:&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;INFO [hf.rs:200] loading tokenizer with fastokens
WARNING [hf.rs:221] failed to load tokenizer with fastokens; falling back to
        HuggingFace tokenizers
        error=tokenizer error: normalizer error: unsupported normalizer type: Replace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fastokens 0.2.1 doesn't implement the Replace normalizer that Gemma 4's tokenizer.json uses, so it falls back to the same HuggingFace tokenizers the Python path uses. Note the fallback is graceful and correct — you just don't get the fast path on &lt;em&gt;this&lt;/em&gt; model yet. It's a coverage gap in a young crate, and one normalizer away from closing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal isn’t wired up for this model:&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;WARNING [multimodal.rs:446] multimodal model spec is not registered; disabling
        image/video support model_id="google/gemma-4-E2B-it" model_type="gemma4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 E2B is a &lt;strong&gt;vision&lt;/strong&gt; model, and gemma4 isn't in the Rust multimodal spec table yet. Text requests behave identically and the endpoint is healthy, so nothing in a normal check reveals it. Also a registration gap rather than a design problem — but check it for your model before you switch, because a healthy endpoint won't tell you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Is it faster?
&lt;/h4&gt;

&lt;p&gt;On a T4G, no. Output token throughput, same engine config, client on the box against localhost:&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%2Fwvz04qrmum4ad8ayvpbt.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%2Fwvz04qrmum4ad8ayvpbt.png" width="583" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Median TTFT tracks just as tightly — 14305 ms against 14311 ms at concurrency 32.&lt;/p&gt;

&lt;p&gt;That’s the expected result, and worth saying plainly: decode on this card is &lt;strong&gt;bandwidth-bound&lt;/strong&gt; at a measured 277 GB/s, and the engine saturates at --max-num-seqs 8. A frontend rewrite targets CPU-side per-request overhead. Here that overhead hides behind the GPU, so swapping it can't move a bottleneck-limited number. &lt;strong&gt;If you want the Rust frontend to buy you tokens per second on a small GPU, it won't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One signal does appear, in median inter-token latency at high concurrency:&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%2Fpgmm8n07nfd1rkq1yi3w.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%2Fpgmm8n07nfd1rkq1yi3w.png" width="712" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mean TPOT barely moves, so this is the middle of the distribution tightening rather than everything speeding up — the shape you’d expect from a frontend scheduling streaming work more evenly once many streams are in flight. Worth knowing if you serve at concurrency; not worth switching for on its own. 📊&lt;/p&gt;

&lt;h4&gt;
  
  
  If a plain pip install -e . already ran
&lt;/h4&gt;

&lt;p&gt;A from-source vLLM install done &lt;strong&gt;without&lt;/strong&gt; the steps above succeeds, exits 0, and leaves you with a 96 MB debug tool parser and no frontend binary. Four defaults stack up to make that silent:&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%2Fmrnq0oww0b360bbo4g6h.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%2Fmrnq0oww0b360bbo4g6h.png" width="798" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VLLM_REQUIRE_RUST_FRONTEND=1 turns the second row into a hard build failure, which is what you want on any machine you plan to serve from.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cheat sheet
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# toolchain — you supply these by hand because the sm_75 requirement&lt;/span&gt;
&lt;span class="c"&gt;# forces --no-build-isolation, which disables pip's automatic build deps&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/env"&lt;/span&gt;
/opt/pytorch/bin/python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;setuptools_rust &lt;span class="c"&gt;# the BUILD interpreter&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; protobuf-compiler cuda-toolkit-13-2

&lt;span class="c"&gt;# build against the DLAMI's torch, not a PyPI one (PyPI aarch64 has no sm_75)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/vllm
python use_existing_torch.py
&lt;span class="nv"&gt;TORCH_CUDA_ARCH_LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7.5 &lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-build-isolation&lt;/span&gt;

&lt;span class="c"&gt;# Rust artifacts, release profile (editable installs default to debug: 100x bigger)&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_REQUIRE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python tools/build_rust.py &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="c"&gt;# confirm&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vllm/vllm-rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; vllm/vllm-rs &lt;span class="nt"&gt;--help&lt;/span&gt;

&lt;span class="c"&gt;# run — `vllm serve`, NOT the api_server module&lt;/span&gt;
&lt;span class="nv"&gt;VLLM_USE_RUST_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 vllm serve &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000

&lt;span class="c"&gt;# verify it's really Rust&lt;/span&gt;
curl &lt;span class="nt"&gt;-si&lt;/span&gt; localhost:8000/health | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^server:'&lt;/span&gt; &lt;span class="c"&gt;# Rust sends none&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; vllm-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Run on EC2&lt;/em&gt; &lt;em&gt;g5g.xlarge and&lt;/em&gt; &lt;em&gt;g5g.4xlarge,&lt;/em&gt; &lt;em&gt;us-east-1a, NVIDIA T4G (SM 7.5). vLLM&lt;/em&gt; &lt;em&gt;0.27.2rc1.dev0+g7f7a32cfe, rustc 1.97.1, setuptools-rust 1.13.0, libprotoc 3.21.12, torch 2.12.0+cu132. Benchmarks are one run per cell for Rust and two for Python; treat the TPOT delta as suggestive.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>gemma4</category>
      <category>vlllm</category>
      <category>mcps</category>
      <category>rust</category>
    </item>
    <item>
      <title>Looker's Native MCP Server with Claude Code</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:00:55 +0000</pubDate>
      <link>https://dev.to/gde/lookers-native-mcp-server-with-claude-code-11j8</link>
      <guid>https://dev.to/gde/lookers-native-mcp-server-with-claude-code-11j8</guid>
      <description>&lt;p&gt;Looker hosts its own MCP server now. This walks through connecting Claude Code to it, pairing it with the Looker CLI, and being clear-eyed about where the tool set stops.&lt;/p&gt;

&lt;h4&gt;
  
  
  The binary you no longer need
&lt;/h4&gt;

&lt;p&gt;Until recently, connecting an agent to Looker meant running MCP Toolbox as a local binary. You downloaded 292 MB onto your laptop, taught it your API credentials, launched it as a stdio subprocess, and kept it updated forever. Every developer needed their own copy, and the server ran on your side of the wire.&lt;/p&gt;

&lt;p&gt;Looker closed that gap. Every Looker (Google Cloud core) and Looker (original) instance now exposes an MCP endpoint on its own base URL:&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/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire install. Ask the endpoint who it is and the joke lands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOKER_MCP_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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="s1"&gt;'Accept: application/json, text/event-stream'&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;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"probe","version":"0"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"prompts"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Toolbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1.4.0+container.release.linux.amd64.d67cfbe"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"name":"Toolbox"&lt;/code&gt;. It is the same software. Google moved it to the other end of the connection and took over running it. Migrating is not a bet on new technology — it is the server you were already running, minus the operational burden.&lt;/p&gt;

&lt;h4&gt;
  
  
  Before you start
&lt;/h4&gt;

&lt;p&gt;Three things, and only the first needs someone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An admin has to switch the server on.&lt;/strong&gt; It lives at &lt;strong&gt;Admin → Platform → Model Context Protocol&lt;/strong&gt;. That page also holds the allowlist of which tools agents may call. A tool switched off there does not exist as far as any client is concerned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need API3 credentials&lt;/strong&gt; — Base URL, Client ID, Client Secret. Looker admin panel, &lt;em&gt;Users → (your user) → Edit Keys&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note the preview limits&lt;/strong&gt; before you plan around them. Customer-hosted instances are not supported. There are no fine-grained scopes — tool access is one global allowlist, not per-user or per-group. Tool-list changes take about 30 seconds to reach clients, which then have to reconnect.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setup, start to finish
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Resolve credentials.&lt;/strong&gt; The setup script prompts for anything it cannot find, writes &lt;code&gt;.env&lt;/code&gt; at mode 600, and derives &lt;code&gt;LOOKER_MCP_URL&lt;/code&gt; from your base URL:&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;Source it, do not execute it. The reason matters and catches everyone once — see step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install the Looker CLI.&lt;/strong&gt; Checksum-verified into the project root:&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;/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;looker-cli 0.4.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Smoke-test the credentials&lt;/strong&gt; before involving an agent. If this fails, nothing downstream will work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk user me
&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;+----+--------------+-------------+----------+
| ID | DISPLAY NAME | IS DISABLED | ROLE IDS |
+----+--------------+-------------+----------+
| 3  | xbill work   | false       | 8        |
|    |              |             | 4        |
|    |              |             | 149      |
+----+--------------+-------------+----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Register the server with Claude Code.&lt;/strong&gt; The whole configuration is four lines, and it holds no secrets:&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;"mcpServers"&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;"looker-managed"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${LOOKER_MCP_URL}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headersHelper"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR:-.}/lk headers"&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;p&gt;Compare that with what it replaced: a &lt;code&gt;bash -c&lt;/code&gt; wrapper that sourced &lt;code&gt;.env&lt;/code&gt;, checked three variables, and exec'd a 292 MB binary with &lt;code&gt;--stdio --prebuilt looker,looker-dev&lt;/code&gt;. Also gone is &lt;code&gt;startup_timeout_sec&lt;/code&gt; — it existed because the binary had to boot and handshake before the client would call it ready. An endpoint that is already running has nothing to wait for.&lt;/p&gt;

&lt;p&gt;Two fields are doing real work here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;headersHelper&lt;/code&gt; names a &lt;em&gt;command&lt;/em&gt;, not a static value. Claude Code runs it on every connection, and again automatically after a &lt;code&gt;401&lt;/code&gt; or &lt;code&gt;403&lt;/code&gt;, retrying the call once with fresh headers. Looker access tokens live one hour; this makes expiry heal itself. A stale token becomes one 401 you never see.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;${LOOKER_MCP_URL}&lt;/code&gt; is expanded &lt;strong&gt;by Claude Code itself&lt;/strong&gt;, not by a shell. A stdio server could source &lt;code&gt;.env&lt;/code&gt; inside its own wrapper; a remote server has no wrapper. The variable must exist in the environment of the process you launch &lt;code&gt;claude&lt;/code&gt; from — which is exactly why step 1 says &lt;em&gt;source&lt;/em&gt;, not execute. If your tools are missing, check this first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Verify.&lt;/strong&gt; Start Claude Code and run &lt;code&gt;/mcp&lt;/code&gt;. You should see &lt;code&gt;looker-managed&lt;/code&gt; connected with 40 tools. The endpoint will confirm the count itself, without credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOKER_MCP_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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="s1"&gt;'Accept: application/json, text/event-stream'&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;'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.result.tools | length'&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;40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metadata is open — &lt;code&gt;initialize&lt;/code&gt; and &lt;code&gt;tools/list&lt;/code&gt; answer unauthenticated, which is what lets a client discover the server before signing in. &lt;code&gt;tools/call&lt;/code&gt; refuses without a token. Nothing touches data anonymously.&lt;/p&gt;

&lt;p&gt;One note on what this authentication actually is. &lt;code&gt;./lk headers&lt;/code&gt; exchanges your API3 key for a short-lived Looker access token. It works, and it is the right call for evaluation, but every action is attributed to the API3 key's user rather than the human who asked. For a shared instance, register an OAuth client instead — the instance advertises the endpoints at &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; and uses PKCE, so there is no client secret to store.&lt;/p&gt;

&lt;h4&gt;
  
  
  An example: from question to file
&lt;/h4&gt;

&lt;p&gt;Here is the pattern the whole setup exists to support. Start with a question, not a query.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which product categories drive the most revenue?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude walks the semantic model. &lt;code&gt;get_models&lt;/code&gt; returns the instance inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;basic_ecomm, intermediate_ecomm, advanced_ecomm       sample_thelook_ecommerce
london_bicycles                                       london_bicycles
gcp_billing_block                                     marketplace_gcp-billing
bq_agent_analytics                                    agent_events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get_explores&lt;/code&gt; on &lt;code&gt;advanced_ecomm&lt;/code&gt; narrows it to two, and &lt;code&gt;get_measures&lt;/code&gt; returns the aggregates that actually exist — &lt;code&gt;order_items.total_sale_price&lt;/code&gt;, &lt;code&gt;order_items.count&lt;/code&gt;, &lt;code&gt;order_items.average_sale_price&lt;/code&gt;. No guessing at column names, and no SQL. The agent is reading the same governed definitions your dashboards use.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;query&lt;/code&gt; runs it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Outerwear &amp;amp; Coats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;971454.48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6711&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;144.76&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="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Jeans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;924765.39&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9428&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;98.09&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="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Sweaters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;630197.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8476&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;74.35&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outerwear leads on revenue with a third fewer items sold than Jeans, because it carries a 48% higher average price. That is the kind of read worth having an agent for: the rows are in its context, so it can reason about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now hand it to the CLI.&lt;/strong&gt; The query shape is settled, so freeze it. Same fields, same sorts, in a file the CLI understands:&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"advanced_ecomm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"view"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"advanced_example_ecommerce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.average_sale_price"&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;"sorts"&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="s2"&gt;"order_items.total_sale_price desc"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk query runquery &lt;span class="nt"&gt;--file&lt;/span&gt; q.json &lt;span class="nt"&gt;--format&lt;/span&gt; csv &lt;span class="nt"&gt;--output&lt;/span&gt; category-revenue.csv
&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;Products Category,Order Items Sales,Order Items # of Order Items,Order Items Average Price
Outerwear &amp;amp; Coats,971454.4791278839,6711,144.75554747845126
Jeans,924765.3913908005,9428,98.08712254887557
Sweaters,630197.0301675797,8476,74.35075863232427
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical numbers, different destination. That is the entire argument for running both.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;view&lt;/code&gt; key is the one spelling trap — the CLI calls the explore &lt;code&gt;view&lt;/code&gt;, while &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;fields&lt;/code&gt;, &lt;code&gt;filters&lt;/code&gt; and &lt;code&gt;sorts&lt;/code&gt; match MCP exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why bother with the second interface at all?&lt;/strong&gt; Because the two differ in what happens to the answer, not in what they can reach. Same instance, same API3 key, same REST API underneath.&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;Native MCP (&lt;code&gt;looker-managed&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;CLI (&lt;code&gt;./lk&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;40 tools: query, content, LookML dev, health&lt;/td&gt;
&lt;td&gt;The whole API — git, users, roles, schedules, connections, deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results land&lt;/td&gt;
&lt;td&gt;In the model's context&lt;/td&gt;
&lt;td&gt;On disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Every row consumes context&lt;/td&gt;
&lt;td&gt;Free of context until you read the file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good at&lt;/td&gt;
&lt;td&gt;Discovery, judgement, structured content creation&lt;/td&gt;
&lt;td&gt;Scale, files, determinism, repeatability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad at&lt;/td&gt;
&lt;td&gt;Bulk output, anything admin-shaped&lt;/td&gt;
&lt;td&gt;Deciding what to ask for&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Discover and decide over MCP, execute and persist with the CLI.&lt;/strong&gt; Asking an agent which explore holds revenue by cohort is worth twenty minutes of clicking through the Explore UI. Routing 40,000 rows through its context is not. Once the shape is settled, the CSV job runs forever with no agent in the loop and no tokens burned.&lt;/p&gt;

&lt;p&gt;The two also meet at the credential: &lt;code&gt;./lk headers&lt;/code&gt; is what authenticates the MCP server in the first place. The CLI is not a second path bolted on beside MCP — it is what gets MCP connected.&lt;/p&gt;

&lt;h4&gt;
  
  
  Native vs MCP Toolbox
&lt;/h4&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;MCP Toolbox (local binary)&lt;/th&gt;
&lt;th&gt;Native (Looker-hosted)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;292 MB download per machine, updated forever&lt;/td&gt;
&lt;td&gt;A URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;stdio subprocess&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;You pin it — currently v1.8.0&lt;/td&gt;
&lt;td&gt;Google pins it — currently 1.4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;46, from &lt;code&gt;--prebuilt looker,looker-dev&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;40, from the admin allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Client-side, per developer, advisory&lt;/td&gt;
&lt;td&gt;Admin panel, instance-wide, enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;API3 key in the subprocess environment&lt;/td&gt;
&lt;td&gt;Bearer token, or OAuth 2.1 + PKCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;"Why won't the server start"&lt;/td&gt;
&lt;td&gt;"Why is the endpoint slow"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rows deserve more than a table cell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version is the real trade.&lt;/strong&gt; The hosted server reports 1.4.0 while the downloadable binary is on v1.8.0. You stop patching, and you also stop choosing. If you depend on something that landed in Toolbox after 1.4.0, stay where you are for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance is the real win.&lt;/strong&gt; With a downloaded Toolbox, the tool set was whatever &lt;code&gt;--prebuilt&lt;/code&gt; shipped, and any restriction had to be re-implemented in every client by every developer who installed it. Now a tool switched off in the admin panel does not exist for anyone. That is the difference between a policy and a suggestion.&lt;/p&gt;

&lt;p&gt;Migrating is mostly deletion: add the new server, move your git workflow to the CLI, then delete the binary, the download step, the launcher script and the &lt;code&gt;toolbox&lt;/code&gt; line in &lt;code&gt;.gitignore&lt;/code&gt;. In this repo that removed 292 MB, a checksum routine, a stdio wrapper, and an entire class of "why won't the server start" support question.&lt;/p&gt;

&lt;h4&gt;
  
  
  What MCP cannot do
&lt;/h4&gt;

&lt;p&gt;The gaps are not random. The server covers Looker as a &lt;em&gt;semantic model&lt;/em&gt; and stops at the edge of Looker as an &lt;em&gt;administered system&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git, entirely.&lt;/strong&gt; &lt;code&gt;list_git_branches&lt;/code&gt;, &lt;code&gt;get_git_branch&lt;/code&gt;, &lt;code&gt;create_git_branch&lt;/code&gt;, &lt;code&gt;switch_git_branch&lt;/code&gt; and &lt;code&gt;delete_git_branch&lt;/code&gt; all shipped with the local binary. The managed server exposes none of them. File editing and &lt;code&gt;dev_mode&lt;/code&gt; are present, so the agent can write LookML — it just cannot get itself onto a branch to write it safely. The CLI covers that half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk api project create_git_branch &lt;span class="nt"&gt;--project_id&lt;/span&gt; my_project &lt;span class="nt"&gt;--name&lt;/span&gt; my_branch
./lk project checkout my_project my_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Commit is missing from both, and that one is not an MCP limitation.&lt;/strong&gt; The Looker API has no commit endpoint at all. Search the CLI's entire surface and you get deploy verbs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk meta search commit
&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;Found 7 matching commands:
  looker-cli api project create_git_branch     - Checkout New Git Branch
  looker-cli api project deploy_to_production  - Deploy To Production
  looker-cli api project tag_ref               - Tag Ref
  looker-cli api project update_git_branch     - Update Project Git Branch
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the agent creates the branch, writes the files, validates them and runs the tests — then stops. Committing the workspace is an IDE operation. The loop that looks like it should close (branch → edit → validate → commit → deploy) closes at every step except the second to last, and that step needs a browser.&lt;/p&gt;

&lt;p&gt;Plan around it rather than fighting it. The agent does the branch, the edits, the validation and the tests; a human commits in the Looker IDE; the terminal deploys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk project deploy my_project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy_to_production&lt;/code&gt; never had an MCP equivalent either, so the deploy was always a CLI call. The commit is the only step neither interface can reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_field_value_suggestions&lt;/code&gt;.&lt;/strong&gt; Gone. To find valid filter values, query the field's suggest explore directly — &lt;code&gt;get_dimensions&lt;/code&gt; names it in the &lt;code&gt;suggest_explore&lt;/code&gt; and &lt;code&gt;suggest_dimension&lt;/code&gt; attributes of any suggestable field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The entire administrative surface.&lt;/strong&gt; Users, groups, roles, permissions, user attributes, schedules, alerts, connections, themes and sessions have no MCP tools and never did. Every one is a &lt;code&gt;./lk api&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Two permission failures look like bugs and are not: &lt;code&gt;health_analyze&lt;/code&gt; and &lt;code&gt;health_vacuum&lt;/code&gt; need System Activity access and return &lt;em&gt;Access Denied&lt;/em&gt; without it, and &lt;code&gt;plan ls&lt;/code&gt; returns 404 without &lt;code&gt;see_schedules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything else — discovery, querying, content creation, LookML files, validation, tests, health — is present and works.&lt;/p&gt;

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

&lt;p&gt;The native install is a smaller thing to own than what it replaced. A 292 MB download, a stdio wrapper and a &lt;code&gt;--prebuilt&lt;/code&gt; flag collapsed into a URL and a &lt;code&gt;headersHelper&lt;/code&gt;. Google patches the server, the admin panel governs which tools exist, and System Activity logs what the agent did.&lt;/p&gt;

&lt;p&gt;What you give up is specific and covered: five git tools and &lt;code&gt;get_field_value_suggestions&lt;/code&gt;, all of which the CLI handles. What you should fix before production is the auth shortcut — swap the API3 token helper for a registered OAuth client so actions are attributed to a person rather than a key.&lt;/p&gt;

&lt;p&gt;The durable lesson is the division of labor. The MCP server is for discovery and judgement; the CLI is for execution and persistence. Install only one and you will find the seam within a week — most likely on a Tuesday afternoon, halfway through a LookML change, at the commit step.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/mcp" rel="noopener noreferrer"&gt;Looker-managed MCP server | Google Cloud Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/admin-panel-platform-mcp" rel="noopener noreferrer"&gt;Admin settings — Model Context Protocol (MCP) | Google Cloud Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/looker-open-source/looker-cli" rel="noopener noreferrer"&gt;Looker CLI | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/googleapis/genai-toolbox" rel="noopener noreferrer"&gt;MCP Toolbox for Databases | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>looker</category>
      <category>claudecode</category>
      <category>cli</category>
    </item>
    <item>
      <title>Looker’s Native MCP Server with Claude Code</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 13:57:39 +0000</pubDate>
      <link>https://dev.to/gde/lookers-native-mcp-server-with-claude-code-38hm</link>
      <guid>https://dev.to/gde/lookers-native-mcp-server-with-claude-code-38hm</guid>
      <description>&lt;p&gt;Looker hosts its own MCP server now. This walks through connecting Claude Code to it, pairing it with the Looker CLI, and being clear-eyed about where the tool set stops.&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%2Fe591g9beemq4x0kvakr2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe591g9beemq4x0kvakr2.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The binary you no longer need
&lt;/h4&gt;

&lt;p&gt;Until recently, connecting an agent to Looker meant running MCP Toolbox as a local binary. You downloaded 292 MB onto your laptop, taught it your API credentials, launched it as a stdio subprocess, and kept it updated forever. Every developer needed their own copy, and the server ran on your side of the wire.&lt;/p&gt;

&lt;p&gt;Looker closed that gap. Every Looker (Google Cloud core) and Looker (original) instance now exposes an MCP endpoint on its own base URL:&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/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire install. Ask the endpoint who it is and the joke lands:&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="err"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-X&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$LOOKER_MCP_URL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;-H&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;-H&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Accept:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;text/event-stream'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"initialize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:{},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"clientInfo"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"probe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"prompts"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Toolbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1.4.0+container.release.linux.amd64.d67cfbe"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"name":"Toolbox". It is the same software. Google moved it to the other end of the connection and took over running it. Migrating is not a bet on new technology — it is the server you were already running, minus the operational burden.&lt;/p&gt;

&lt;h4&gt;
  
  
  Before you start
&lt;/h4&gt;

&lt;p&gt;Three things, and only the first needs someone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An admin has to switch the server on.&lt;/strong&gt; It lives at &lt;strong&gt;Admin → Platform → Model Context Protocol&lt;/strong&gt;. That page also holds the allowlist of which tools agents may call. A tool switched off there does not exist as far as any client is concerned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need API3 credentials&lt;/strong&gt;  — Base URL, Client ID, Client Secret. Looker admin panel, &lt;em&gt;Users → (your user) → Edit Keys&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note the preview limits&lt;/strong&gt; before you plan around them. Customer-hosted instances are not supported. There are no fine-grained scopes — tool access is one global allowlist, not per-user or per-group. Tool-list changes take about 30 seconds to reach clients, which then have to reconnect.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setup, start to finish
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Resolve credentials.&lt;/strong&gt; The setup script prompts for anything it cannot find, and derives LOOKER_MCP_URL from your base URL:&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;Source it, do not execute it. The reason matters and catches everyone once — see step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install the Looker CLI.&lt;/strong&gt; Checksum-verified into the project root:&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

looker-cli 0.4.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Smoke-test the credentials&lt;/strong&gt; before involving an agent. If this fails, nothing downstream will work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./lk user me

+----+--------------+-------------+----------+
| ID | DISPLAY NAME | IS DISABLED | ROLE IDS |
+----+--------------+-------------+----------+
| 3 | xbill work | false | 8 |
| | | | 4 |
| | | | 149 |
+----+--------------+-------------+----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Register the server with Claude Code.&lt;/strong&gt; The whole configuration is four lines, and it holds no secrets:&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;"mcpServers"&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;"looker-managed"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${LOOKER_MCP_URL}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headersHelper"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR:-.}/lk headers"&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;p&gt;Compare that with what it replaced: a bash -c wrapper that sourced .env, checked three variables, and exec’d a 292 MB binary with --stdio --prebuilt looker,looker-dev. Also gone is startup_timeout_sec — it existed because the binary had to boot and handshake before the client would call it ready. An endpoint that is already running has nothing to wait for.&lt;/p&gt;

&lt;p&gt;Two fields are doing real work here.&lt;/p&gt;

&lt;p&gt;headersHelper names a &lt;em&gt;command&lt;/em&gt;, not a static value. Claude Code runs it on every connection, and again automatically after a 401 or 403, retrying the call once with fresh headers. Looker access tokens live one hour; this makes expiry heal itself. A stale token becomes one 401 you never see.&lt;/p&gt;

&lt;p&gt;${LOOKER_MCP_URL} is expanded &lt;strong&gt;by Claude Code itself&lt;/strong&gt; , not by a shell. A stdio server could source .env inside its own wrapper; a remote server has no wrapper. The variable must exist in the environment of the process you launch claude from — which is exactly why step 1 says &lt;em&gt;source&lt;/em&gt;, not execute. If your tools are missing, check this first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Verify.&lt;/strong&gt; Start Claude Code and run /mcp. You should see looker-managed connected with 40 tools. The endpoint will confirm the count itself, without credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOOKER_MCP_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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="s1"&gt;'Accept: application/json, text/event-stream'&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;'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.result.tools | length'&lt;/span&gt;

40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metadata is open — initialize and tools/list answer unauthenticated, which is what lets a client discover the server before signing in. tools/call refuses without a token. Nothing touches data anonymously.&lt;/p&gt;

&lt;p&gt;One note on what this authentication actually is. ./lk headers exchanges your API3 key for a short-lived Looker access token. It works, and it is the right call for evaluation, but every action is attributed to the API3 key’s user rather than the human who asked. For a shared instance, register an OAuth client instead — the instance advertises the endpoints at /.well-known/oauth-authorization-server and uses PKCE, so there is no client secret to store.&lt;/p&gt;

&lt;h4&gt;
  
  
  An example: from question to file
&lt;/h4&gt;

&lt;p&gt;Here is the pattern the whole setup exists to support. Start with a question, not a query.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Which product categories drive the most revenue?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude walks the semantic model. get_models returns the instance inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;basic_ecomm, intermediate_ecomm, advanced_ecomm sample_thelook_ecommerce
london_bicycles london_bicycles
gcp_billing_block marketplace_gcp-billing
bq_agent_analytics agent_events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;get_explores on advanced_ecomm narrows it to two, and get_measures returns the aggregates that actually exist — order_items.total_sale_price, order_items.count, order_items.average_sale_price. No guessing at column names, and no SQL. The agent is reading the same governed definitions your dashboards use.&lt;/p&gt;

&lt;p&gt;Then query runs it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Outerwear &amp;amp; Coats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;971454.48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6711&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;144.76&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="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Jeans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;924765.39&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9428&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;98.09&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="nl"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Sweaters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;630197.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8476&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"order_items.average_sale_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;74.35&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outerwear leads on revenue with a third fewer items sold than Jeans, because it carries a 48% higher average price. That is the kind of read worth having an agent for: the rows are in its context, so it can reason about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now hand it to the CLI.&lt;/strong&gt; The query shape is settled, so freeze it. Same fields, same sorts, in a file the CLI understands:&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"advanced_ecomm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"view"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"advanced_example_ecommerce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"products.category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.total_sale_price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_items.average_sale_price"&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;"sorts"&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="s2"&gt;"order_items.total_sale_price desc"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6"&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="err"&gt;./lk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;runquery&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;q.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;category-revenue.csv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Category,Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sales,Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Items,Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Average&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Price&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Outerwear&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Coats,&lt;/span&gt;&lt;span class="mf"&gt;971454.4791278839&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6711&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;144.75554747845126&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Jeans,&lt;/span&gt;&lt;span class="mf"&gt;924765.3913908005&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9428&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;98.08712254887557&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Sweaters,&lt;/span&gt;&lt;span class="mf"&gt;630197.0301675797&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8476&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;74.35075863232427&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical numbers, different destination. That is the entire argument for running both.&lt;/p&gt;

&lt;p&gt;The view key is the one spelling trap — the CLI calls the explore view, while model, fields, filters and sorts match MCP exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why bother with the second interface at all?&lt;/strong&gt; Because the two differ in what happens to the answer, not in what they can reach. Same instance, same API3 key, same REST API underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native MCP (&lt;/strong&gt;&lt;strong&gt;looker-managed)&lt;/strong&gt; - Coverage: 40 tools — query, content, LookML dev, health - Results land in the model’s context - Every row consumes context - Good at: discovery, judgement, structured content creation - Bad at: bulk output, anything admin-shaped&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI (&lt;/strong&gt;&lt;strong&gt;./lk)&lt;/strong&gt; - Coverage: the whole API — git, users, roles, schedules, connections, deploys - Results land on disk - Free of context until you read the file - Good at: scale, files, determinism, repeatability - Bad at: deciding what to ask for&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discover and decide over MCP, execute and persist with the CLI.&lt;/strong&gt; Asking an agent which explore holds revenue by cohort is worth twenty minutes of clicking through the Explore UI. Routing 40,000 rows through its context is not. Once the shape is settled, the CSV job runs forever with no agent in the loop and no tokens burned.&lt;/p&gt;

&lt;p&gt;The two also meet at the credential: ./lk headers is what authenticates the MCP server in the first place. The CLI is not a second path bolted on beside MCP — it is what gets MCP connected.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;MCP Toolbox (local binary)&lt;/strong&gt; — Install: 292 MB download per machine, updated forever — Transport: stdio subprocess — Version: you pin it — currently v1.8.0 — Tools: 46, from --prebuilt looker,looker-dev - Governance: client-side, per developer, advisory - Auth: API3 key in the subprocess environment - Failure mode: “why won’t the server start”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native (Looker-hosted)&lt;/strong&gt; — Install: a URL — Transport: streamable HTTP — Version: Google pins it — currently 1.4.0 — Tools: 40, from the admin allowlist — Governance: admin panel, instance-wide, enforced — Auth: bearer token, or OAuth 2.1 + PKCE — Failure mode: “why is the endpoint slow”&lt;/p&gt;

&lt;p&gt;Two rows deserve more than a table cell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version is the real trade.&lt;/strong&gt; The hosted server reports 1.4.0 while the downloadable binary is on v1.8.0. You stop patching, and you also stop choosing. If you depend on something that landed in Toolbox after 1.4.0, stay where you are for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance is the real win.&lt;/strong&gt; With a downloaded Toolbox, the tool set was whatever --prebuilt shipped, and any restriction had to be re-implemented in every client by every developer who installed it. Now a tool switched off in the admin panel does not exist for anyone. That is the difference between a policy and a suggestion.&lt;/p&gt;

&lt;p&gt;Migrating is mostly deletion: add the new server, move your git workflow to the CLI, then delete the binary, the download step, the launcher script and the toolbox line in .gitignore. In this repo that removed 292 MB, a checksum routine, a stdio wrapper, and an entire class of “why won’t the server start” support question.&lt;/p&gt;

&lt;h4&gt;
  
  
  What MCP cannot do
&lt;/h4&gt;

&lt;p&gt;The gaps are not random. The server covers Looker as a &lt;em&gt;semantic model&lt;/em&gt; and stops at the edge of Looker as an &lt;em&gt;administered system&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git, entirely.&lt;/strong&gt; list_git_branches, get_git_branch, create_git_branch, switch_git_branch and delete_git_branch all shipped with the local binary. The managed server exposes none of them. File editing and dev_mode are present, so the agent can write LookML — it just cannot get itself onto a branch to write it safely. The CLI covers that half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk api project create_git_branch &lt;span class="nt"&gt;--project_id&lt;/span&gt; my_project &lt;span class="nt"&gt;--name&lt;/span&gt; my_branch
./lk project checkout my_project my_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Commit is missing from both, and that one is not an MCP limitation.&lt;/strong&gt; The Looker API has no commit endpoint at all. Search the CLI’s entire surface and you get deploy verbs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./lk meta search commit

Found 7 matching commands:
  looker-cli api project create_git_branch - Checkout New Git Branch
  looker-cli api project deploy_to_production - Deploy To Production
  looker-cli api project tag_ref - Tag Ref
  looker-cli api project update_git_branch - Update Project Git Branch
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the agent creates the branch, writes the files, validates them and runs the tests — then stops. Committing the workspace is an IDE operation. The loop that looks like it should close (branch → edit → validate → commit → deploy) closes at every step except the second to last, and that step needs a browser.&lt;/p&gt;

&lt;p&gt;Plan around it rather than fighting it. The agent does the branch, the edits, the validation and the tests; a human commits in the Looker IDE; the terminal deploys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./lk project deploy my_project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;deploy_to_production never had an MCP equivalent either, so the deploy was always a CLI call. The commit is the only step neither interface can reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;get_field_value_suggestions.&lt;/strong&gt; Gone. To find valid filter values, query the field’s suggest explore directly — get_dimensions names it in the suggest_explore and suggest_dimension attributes of any suggestable field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The entire administrative surface.&lt;/strong&gt; Users, groups, roles, permissions, user attributes, schedules, alerts, connections, themes and sessions have no MCP tools and never did. Every one is a ./lk api call.&lt;/p&gt;

&lt;p&gt;Two permission failures look like bugs and are not: health_analyze and health_vacuum need System Activity access and return &lt;em&gt;Access Denied&lt;/em&gt; without it, and plan ls returns 404 without see_schedules.&lt;/p&gt;

&lt;p&gt;Everything else — discovery, querying, content creation, LookML files, validation, tests, health — is present and works.&lt;/p&gt;

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

&lt;p&gt;The native install is a smaller thing to own than what it replaced. A 292 MB download, a stdio wrapper and a --prebuilt flag collapsed into a URL and a headersHelper. Google patches the server, the admin panel governs which tools exist, and System Activity logs what the agent did.&lt;/p&gt;

&lt;p&gt;What you give up is specific and covered: five git tools and get_field_value_suggestions, all of which the CLI handles. What you should fix before production is the auth shortcut — swap the API3 token helper for a registered OAuth client so actions are attributed to a person rather than a key.&lt;/p&gt;

&lt;p&gt;The durable lesson is the division of labor. The MCP server is for discovery and judgement; the CLI is for execution and persistence. Install only one and you will find the seam within a week — most likely on a Tuesday afternoon, halfway through a LookML change, at the commit step.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/mcp" rel="noopener noreferrer"&gt;Looker-managed MCP server | Google Cloud Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/looker/docs/admin-panel-platform-mcp" rel="noopener noreferrer"&gt;Admin settings — Model Context Protocol (MCP) | Google Cloud Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/looker-open-source/looker-cli" rel="noopener noreferrer"&gt;Looker CLI | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/googleapis/genai-toolbox" rel="noopener noreferrer"&gt;MCP Toolbox for Databases | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>looker</category>
      <category>mcpserver</category>
      <category>oauth</category>
      <category>lookml</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:45:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/running-gemma-4-on-ec2-g5g-graviton2-amd-with-nvidia-gpu-13j</link>
      <guid>https://dev.to/aws-builders/running-gemma-4-on-ec2-g5g-graviton2-amd-with-nvidia-gpu-13j</guid>
      <description>&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>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>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>
  </channel>
</rss>
