<?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: Russell Van Curen</title>
    <description>The latest articles on DEV Community by Russell Van Curen (@vancuren).</description>
    <link>https://dev.to/vancuren</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%2F159644%2Facfcba80-36e4-4a2e-a754-f5cf1d79df34.jpg</url>
      <title>DEV Community: Russell Van Curen</title>
      <link>https://dev.to/vancuren</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vancuren"/>
    <language>en</language>
    <item>
      <title>Using Codex and Claude Code through one gateway: setup guide</title>
      <dc:creator>Russell Van Curen</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:57:40 +0000</pubDate>
      <link>https://dev.to/tokenrouter/using-codex-and-claude-code-through-one-gateway-setup-guide-3h5</link>
      <guid>https://dev.to/tokenrouter/using-codex-and-claude-code-through-one-gateway-setup-guide-3h5</guid>
      <description>&lt;p&gt;Plenty of teams don't standardize on one coding agent. Some engineers prefer Claude Code, others Codex, and a few switch depending on the task, or on whose usage limit reset last. That's fine until someone asks what the team spent on coding agents last month, and the answer spans two providers, several shared keys and a spreadsheet.&lt;/p&gt;

&lt;p&gt;This guide sets up both tools to go through one gateway. Every engineer gets one key with one budget, and every session lands in one set of logs, whichever agent they ran. We use TokenRouter, which we build, but the tool-side config also shows what to look for in any gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works: two protocols, one gateway
&lt;/h2&gt;

&lt;p&gt;Claude Code speaks the &lt;strong&gt;Anthropic Messages API&lt;/strong&gt; (&lt;code&gt;/v1/messages&lt;/code&gt;). Codex CLI speaks the &lt;strong&gt;OpenAI Responses API&lt;/strong&gt; (&lt;code&gt;/v1/responses&lt;/code&gt;) and supports custom model providers. A gateway that serves both natively can sit in front of both tools, with no wrappers or forks.&lt;/p&gt;

&lt;p&gt;TokenRouter serves &lt;code&gt;/v1/messages&lt;/code&gt; natively for Anthropic clients, passing requests to Claude models through verbatim. For OpenAI-shaped clients it serves &lt;code&gt;/v1/responses&lt;/code&gt;, plus &lt;code&gt;/v1/chat/completions&lt;/code&gt; and &lt;code&gt;/v1/embeddings&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: connect providers, set budgets, create keys
&lt;/h2&gt;

&lt;p&gt;In the console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect providers.&lt;/strong&gt; Open &lt;strong&gt;Provider Keys → Add Provider Key&lt;/strong&gt; and add each provider you'll use, for example Anthropic and OpenAI. The Free plan connects 2 providers, and paid plans connect all 13.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invite the engineers.&lt;/strong&gt; Under &lt;strong&gt;Members → Invite member&lt;/strong&gt;, add each engineer with the Member role and pick their team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a daily hard cap per engineer.&lt;/strong&gt; Under &lt;strong&gt;Budgets → Create budget&lt;/strong&gt;, set Scope to Member, Period to Daily, a dollar limit, and the hard-cap switch on. Alerts go out at 50%, 80% and 100% by default. Daily budgets reset at 00:00 UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have each engineer create their own key&lt;/strong&gt; under &lt;strong&gt;API Keys → Create key&lt;/strong&gt;, with an RPM and TPM limit. Keys a member creates are tied to them, so their member budget covers every key they own. That means one budget covers both tools, whether they use one key for both or one key per tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One key for both tools (say &lt;code&gt;agents-alice&lt;/code&gt;) is simplest. One key per tool (&lt;code&gt;claude-code-alice&lt;/code&gt;, &lt;code&gt;codex-alice&lt;/code&gt;) gives you a clean Claude Code vs Codex cost comparison, under the same person's budget.&lt;/p&gt;

&lt;p&gt;When a hard cap would be crossed, the request is refused before it reaches the provider with &lt;code&gt;429 budget_exceeded&lt;/code&gt;. Rate-limit breaches return &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Claude Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.tokenrouter.io
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tr_your_key_here
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's &lt;strong&gt;no &lt;code&gt;/v1&lt;/code&gt;&lt;/strong&gt; on this base URL. Anthropic clients append &lt;code&gt;/v1/messages&lt;/code&gt; themselves. &lt;code&gt;ANTHROPIC_API_KEY=tr_...&lt;/code&gt; works too. Add both lines to your shell profile.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Reply with exactly: routed via TokenRouter"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Codex CLI
&lt;/h2&gt;

&lt;p&gt;Add a provider to &lt;code&gt;~/.codex/config.toml&lt;/code&gt;. &lt;strong&gt;This base URL does include &lt;code&gt;/v1&lt;/code&gt;&lt;/strong&gt;, the opposite of Claude Code:&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="py"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openai/gpt-5-mini"&lt;/span&gt;
&lt;span class="py"&gt;model_provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tokenrouter"&lt;/span&gt;

&lt;span class="nn"&gt;[model_providers.tokenrouter]&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;"TokenRouter"&lt;/span&gt;
&lt;span class="py"&gt;base_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.tokenrouter.io/v1"&lt;/span&gt;
&lt;span class="py"&gt;env_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TOKENROUTER_API_KEY"&lt;/span&gt;
&lt;span class="py"&gt;wire_api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"responses"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TOKENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tr_your_key_here
codex &lt;span class="s2"&gt;"explain what this repo does"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex sends stateless Responses calls and resends the context each turn. That matches TokenRouter's Responses implementation, so no &lt;code&gt;previous_response_id&lt;/code&gt; is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 (optional): opencode
&lt;/h2&gt;

&lt;p&gt;opencode takes any OpenAI-compatible provider. In &lt;code&gt;opencode.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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://opencode.ai/config.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&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;"tokenrouter"&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;"npm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@ai-sdk/openai-compatible"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TokenRouter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"baseURL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.tokenrouter.io/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{env:TOKENROUTER_API_KEY}"&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;"models"&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;"openai/gpt-5-mini"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GPT-5 mini"&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;"anthropic/claude-sonnet-4-5"&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;"name"&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 Sonnet 4.5"&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;"auto"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TokenRouter Auto"&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;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;h2&gt;
  
  
  Step 5: pick models per tool, or let the router pick
&lt;/h2&gt;

&lt;p&gt;Models are addressed as &lt;code&gt;provider/model&lt;/code&gt; across all 13 providers, and the same ID works on every endpoint that takes a model field. Any routed model works in Codex's &lt;code&gt;model&lt;/code&gt; field, including Anthropic models such as &lt;code&gt;anthropic/claude-sonnet-4-5&lt;/code&gt;. TokenRouter translates Responses calls to each provider's native API, so Codex can run against a Claude model through the same key:&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="py"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"anthropic/claude-sonnet-4-5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On paid plans you can pass &lt;code&gt;auto&lt;/code&gt; with a strategy: &lt;code&gt;auto:cost&lt;/code&gt;, &lt;code&gt;auto:latency&lt;/code&gt;, &lt;code&gt;auto:quality&lt;/code&gt; or &lt;code&gt;auto:balanced&lt;/code&gt;. The router picks among providers you have keys for and respects the team's model allowlist. The chosen model comes back in the response's &lt;code&gt;model&lt;/code&gt; field and shows up in analytics, so attribution stays exact. On the Free plan, &lt;code&gt;auto&lt;/code&gt; returns &lt;code&gt;403 auto_routing_unavailable&lt;/code&gt;, so pass an explicit model.&lt;/p&gt;

&lt;p&gt;A reasonable pattern: keep agents on explicit models you've tested, and use &lt;code&gt;auto:cost&lt;/code&gt; for side jobs like summarizing logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: one place to see spend
&lt;/h2&gt;

&lt;p&gt;Every request from either tool is tied to a key, and a member-created key also ties it to that engineer and their team. Analytics and CSV exports break spend down by team, member, key, model and provider. With separate keys per tool, that's also a direct Claude Code vs Codex cost comparison for your own team. It's more useful than per-token prices, because the same model can cost very different amounts per task depending on the harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout checklist for a team
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start with two volunteers, one on each tool, for a week. Use member budgets with the hard-cap switch off, so nobody gets blocked while you learn what normal looks like.&lt;/li&gt;
&lt;li&gt;Find each person's heaviest normal day in analytics, then turn the hard cap on with a limit comfortably above it. The cap is there to stop loops, not to ration work.&lt;/li&gt;
&lt;li&gt;Set a TPM limit on every key, so a retry loop gets throttled in minutes instead of discovered at the end of the day.&lt;/li&gt;
&lt;li&gt;Once you know which models the team actually uses, add a team model allowlist (Model Access).&lt;/li&gt;
&lt;li&gt;Write the two env vars and the Codex provider block into your onboarding doc, including the &lt;code&gt;/v1&lt;/code&gt; difference between them.&lt;/li&gt;
&lt;li&gt;Offboarding becomes simple: revoke that person's keys and remove them from Members.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Known issues to test before a team rollout
&lt;/h2&gt;

&lt;p&gt;Both tools change fast, and some open issues affect any gateway or custom provider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code:&lt;/strong&gt; the desktop app's Code tab ignores &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; from settings.json (#97574). Any non-default base URL drops message threads and reduces parallel tool calls in headless runs (#98464). &lt;code&gt;/model&lt;/code&gt; on resumed sessions may not show proxied models (#98540).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex:&lt;/strong&gt; several open issues report custom-provider sessions failing to archive, or losing the provider's model catalog after an update (openai/codex#48367, #48961, #49227, #50282). Pin versions for team rollouts and recheck after upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;404 from Claude Code:&lt;/strong&gt; check for &lt;code&gt;/v1&lt;/code&gt; on &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403 &lt;code&gt;model_not_allowed&lt;/code&gt;:&lt;/strong&gt; the model isn't on that team's allowlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 &lt;code&gt;budget_exceeded&lt;/code&gt; vs &lt;code&gt;rate_limit_exceeded&lt;/code&gt;:&lt;/strong&gt; the first means the request would go past a hard cap for the current period. The second means slow down and retry after the &lt;code&gt;Retry-After&lt;/code&gt; delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 &lt;code&gt;upstream_error&lt;/code&gt;:&lt;/strong&gt; the gateway couldn't get a good response from the provider on your key.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;We build TokenRouter. The free plan needs no card. Setup guides for Claude Code, Codex, opencode and zcode are in the docs: &lt;a href="https://www.tokenrouter.io/docs/codex" rel="noopener noreferrer"&gt;tokenrouter.io/docs/codex&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>codex</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running LLM-as-a-judge on live traffic without blowing your budget</title>
      <dc:creator>Russell Van Curen</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:59:55 +0000</pubDate>
      <link>https://dev.to/tokenrouter/running-llm-as-a-judge-on-live-traffic-without-blowing-your-budget-2o32</link>
      <guid>https://dev.to/tokenrouter/running-llm-as-a-judge-on-live-traffic-without-blowing-your-budget-2o32</guid>
      <description>&lt;p&gt;Offline evals tell you a model did well on the examples you thought of. Live traffic tells you what it's actually doing. The obvious way to watch live traffic is to have a strong model grade every response. That's also the fastest way to make evals cost more than the feature they're watching.&lt;/p&gt;

&lt;p&gt;This guide covers how to run an LLM judge on production traffic at a cost you won't notice. The principles apply to any setup. Where it helps, we show how it works in TokenRouter's monitors, which we build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 1: deterministic checks on everything, the judge on a sample
&lt;/h2&gt;

&lt;p&gt;Some failures don't need judgment. A leaked API key matches a regex. A broken JSON response fails a parser. Those checks are cheap, so run them inline on every request and block when they match.&lt;/p&gt;

&lt;p&gt;A judge model is for judgment calls. "Does this answer cite a document we don't have?" "Did it follow an instruction injected through a retrieved page?" Those need a model, and they don't need to run on every request.&lt;/p&gt;

&lt;p&gt;In TokenRouter these are two separate features. &lt;strong&gt;Rules&lt;/strong&gt; are expressions evaluated in-process on every request or response, with actions like block, redact, transform, route or alert, and an &lt;code&gt;observe&lt;/code&gt; mode for rolling a rule out safely. &lt;strong&gt;Monitors&lt;/strong&gt; are the judge, async and sampled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 2: sample deterministically
&lt;/h2&gt;

&lt;p&gt;Random sampling is fine for averages and bad for debugging. If you rerun a check, or compare Tuesday with Wednesday, you want to judge the same slice. Hash a stable request ID into the decision, so the same request always gets the same answer.&lt;/p&gt;

&lt;p&gt;A minimal version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor_id&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="n"&gt;request_id&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="n"&gt;rate_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;monitor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request_id&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;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rate_pct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Including the monitor ID means two monitors at 10% don't judge the identical 10%. TokenRouter monitors sample this way: you set the rate per monitor, and the same request always makes the same sampling decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 3: never put the judge in the request path
&lt;/h2&gt;

&lt;p&gt;A judge that runs inline adds latency to every sampled request and gives production a new way to fail. Run it after the response. Monitors run seconds after the response, off the request path, so they can't add latency, block a request, or touch a stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 4: make each judgment cheap
&lt;/h2&gt;

&lt;p&gt;Three levers set the cost of each judgment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model.&lt;/strong&gt; Start with a small, cheap judge and move up only if it can't do the job. In TokenRouter the judge has to be a tool-capable model, because verdicts come back through a tool call (more on that below), and the model picker only lists those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input size.&lt;/strong&gt; Truncate what the judge reads. Monitors cap the input at about 4k tokens per judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rubric shape.&lt;/strong&gt; One narrow question with a yes/no verdict and a reason. A 1-10 score per response is harder to act on and harder to calibrate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Principle 5: give the judge its own budget
&lt;/h2&gt;

&lt;p&gt;A judge is just more LLM traffic, so it needs a cap like any other traffic. In TokenRouter, each monitor has a &lt;strong&gt;daily judge budget&lt;/strong&gt;. When it's used up, the monitor pauses itself, and the pause clears at midnight UTC. Leave the field empty and there's no cap, so set one.&lt;/p&gt;

&lt;p&gt;Judge calls run on your own provider keys with zero markup and go through the gateway like any request: rate-limited, logged with &lt;code&gt;source=monitor&lt;/code&gt;, and counted against your organization's default team. A team budget on that team is a second backstop, and the judge's spend sits in analytics next to the traffic it reviewed.&lt;/p&gt;

&lt;p&gt;Our docs give a rough figure: at 100,000 requests a month with 10% sampling, a mini-class judge costs roughly $2 to $10 a month of your own provider spend. Your prompt sizes will move that, so watch the &lt;code&gt;source=monitor&lt;/code&gt; spend for the first week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 6: treat the judge as an attack surface
&lt;/h2&gt;

&lt;p&gt;If the judge reads user content, it can be prompt-injected like any other model. Two defenses help. Present the transcript to the judge as data rather than instructions. And make it return its verdict through a forced tool call, not free text. Monitors do both. Even so, no judge is perfect, so the actions are graded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flag&lt;/strong&gt; (always on): the finding goes to a review queue with a severity, categories and the judge's reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notify&lt;/strong&gt; (opt-in): org admins get an email, at most one per cooldown window, with repeat findings batched into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable key&lt;/strong&gt; (opt-in, with a severity gate): the offending API key is disabled, admins are emailed, the action is audit-logged, and re-enabling it is one click on the API Keys page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat auto-disable as a circuit breaker for severe cases, not a content filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 7: calibrate before you trust the dashboard
&lt;/h2&gt;

&lt;p&gt;Before pointing a judge at live traffic, hand-label 50 to 100 real exchanges, run the judge on the same set, and read every disagreement. Many turn out to be a vague rubric rather than a bad model. Tighten the rubric and repeat. After launch, keep reading the judge's reasoning on each finding. That's how you notice it drifting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 8: only capture what you mean to judge
&lt;/h2&gt;

&lt;p&gt;You can't judge what you didn't keep, and keeping everything is its own risk. By default TokenRouter stores no prompt or response bodies. Request logs carry token counts, cost and latency only.&lt;/p&gt;

&lt;p&gt;Capture is opt-in for a key, a team or a member, and a request is captured when any scope covering it has capture enabled. Captured bodies are compressed and encrypted before they leave the API process. Capture can never fail, slow down or alter a request. Retention is capped by plan: 7 days on Starter, 30 on Team, 90 on Enterprise. You can choose a shorter window, and admins can purge everything at once.&lt;/p&gt;

&lt;p&gt;Monitors only read captured traffic, so start by opting in one team or one key, not the whole organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 9: turn findings into tests
&lt;/h2&gt;

&lt;p&gt;A flagged answer is the best eval example you'll get. From a captured transcript, &lt;strong&gt;Add to dataset&lt;/strong&gt; turns it into a test case. Dataset items are snapshots, so retention sweeps and purges never break your evals.&lt;/p&gt;

&lt;p&gt;Eval runs grade with &lt;code&gt;exact_match&lt;/code&gt;, &lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;regex&lt;/code&gt;, or &lt;code&gt;llm_rubric&lt;/code&gt;, a judge scoring against your rubric. That makes "prove a cheaper model scores the same on our traffic, then switch" something you can measure instead of argue about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together in TokenRouter
&lt;/h2&gt;

&lt;p&gt;A reasonable first setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn on capture for one team.&lt;/strong&gt; Go to &lt;strong&gt;Settings → Data capture&lt;/strong&gt; and add a capture scope (Team, Member or API key). Or use &lt;strong&gt;Enable capture&lt;/strong&gt; on a single key from its menu on the API Keys page. Changes reach the gateway within a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a monitor.&lt;/strong&gt; Go to &lt;strong&gt;Monitors → Create monitor&lt;/strong&gt; and fill in:

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; Whole organization, Team, Member or API key, plus a target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge model:&lt;/strong&gt; a cheap, tool-capable one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample rate (%):&lt;/strong&gt; start at 5 to 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What should it watch for?:&lt;/strong&gt; one narrow, yes/no rubric&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set the Daily judge budget ($).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave notifications and key disables off for the first week.&lt;/strong&gt; Review findings and tune the rubric.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the real failures to a dataset.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plans matter here. Monitors need capture, which starts on Starter. Starter includes 1 judge monitor, Team includes 5, and Enterprise is unlimited. Datasets and eval runs are on Team and Enterprise. The Free plan includes neither capture nor monitors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We build TokenRouter. If you need deep tracing, experiments and CI gating, dedicated eval platforms do that well, and plenty of setups use both. Our monitor docs are at &lt;a href="https://www.tokenrouter.io/docs/monitors" rel="noopener noreferrer"&gt;tokenrouter.io/docs/monitors&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>llm</category>
      <category>llmops</category>
    </item>
    <item>
      <title>How to give every engineer a Claude Code key with a hard daily cap</title>
      <dc:creator>Russell Van Curen</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:24:52 +0000</pubDate>
      <link>https://dev.to/tokenrouter/how-to-give-every-engineer-a-claude-code-key-with-a-hard-daily-cap-4bkj</link>
      <guid>https://dev.to/tokenrouter/how-to-give-every-engineer-a-claude-code-key-with-a-hard-daily-cap-4bkj</guid>
      <description>&lt;p&gt;If your team runs Claude Code on a company Anthropic API key, two questions come up sooner or later. Who spent what last week? And what stops an agent stuck in a loop at 2am from spending the rest of the month?&lt;/p&gt;

&lt;p&gt;With one shared key, the honest answers are "we don't know" and "nothing". The fix is boring and effective: give each engineer their own key, give each engineer a hard daily cap, and check that cap before the request reaches Anthropic. This guide covers why that works, the options you have, and the exact setup with TokenRouter, the gateway we build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a shared key fails
&lt;/h2&gt;

&lt;p&gt;A shared key has three problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No attribution.&lt;/strong&gt; Anthropic's usage console shows spend per key. If everyone uses the same key, you see one number. Anthropic's own Claude apps gateway docs say it plainly: with one shared upstream credential, "your provider's bill attributes everything to that credential, not to individual developers."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No per-person limit.&lt;/strong&gt; Any limit on a shared key is a limit on the whole team. One runaway session can use up everyone's budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offboarding means rotating the key&lt;/strong&gt; for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a useful cap looks like
&lt;/h2&gt;

&lt;p&gt;A cap is only useful if it does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It blocks. It doesn't just report.&lt;/strong&gt; An alert that arrives after the money is spent is a receipt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It's per person, not per company.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It fails in a way tools can see.&lt;/strong&gt; An agent should get a clear error, not something that looks like a normal reply. There's an open Claude Code issue (anthropics/claude-code#98441) where a plan cutoff arrives as an ordinary assistant turn, so subagents can't tell it apart from a real answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's checked before the call, not after.&lt;/strong&gt; Claude Code's &lt;code&gt;--max-budget-usd&lt;/code&gt; flag is checked after each call returns, and an open issue (#100111) measured a $1 cap stopping at $1.38. That's fine for a single headless run. It's a weak bound for a whole day of agent traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Your options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-budget-usd&lt;/code&gt;&lt;/strong&gt;: a per-run bound for headless runs, not a per-person budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Team or Enterprise seats&lt;/strong&gt;: per-member limits for seat users, but not for raw API-key usage in scripts and CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic's Claude apps gateway&lt;/strong&gt;: self-hosted, with per-user, per-group and org spend limits, for Claude traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A self-hosted proxy&lt;/strong&gt; with virtual keys and budgets, such as the open-source LiteLLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hosted gateway with per-person budgets&lt;/strong&gt;, which is where TokenRouter fits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup with TokenRouter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Connect your Anthropic key.&lt;/strong&gt; In the console, open &lt;strong&gt;Provider Keys → Add Provider Key&lt;/strong&gt;, pick Anthropic, and paste the key. Provider keys are AES-encrypted at rest, decrypted only in memory at request time, and never written to logs. Anthropic keeps billing you directly at your rates. We charge a flat subscription and take nothing per token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create a team and invite engineers.&lt;/strong&gt; The structure is organization → teams → members → keys. Create a team under &lt;strong&gt;Teams → Create team&lt;/strong&gt; (for example &lt;code&gt;platform&lt;/code&gt; or &lt;code&gt;mobile&lt;/code&gt;). Then go to &lt;strong&gt;Members → Invite member&lt;/strong&gt;, enter the engineer's email, leave the role as &lt;strong&gt;Member&lt;/strong&gt;, and pick the team. Seats depend on the plan: Free has 1 seat and 1 team, Starter has 5 seats, and Team has 25.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Give each engineer a daily hard cap.&lt;/strong&gt; Open &lt;strong&gt;Budgets → Create budget&lt;/strong&gt; and set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; Member, then pick the engineer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Period:&lt;/strong&gt; Daily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit (USD):&lt;/strong&gt; your number (see "Choosing the number" below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert thresholds:&lt;/strong&gt; 50%, 80% and 100% are preselected, and you can change them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard cap:&lt;/strong&gt; on. The dialog spells out what that means: "Requests beyond the limit are rejected with a 429 until the period resets."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A member budget covers every key that engineer owns. You can also put a team budget on top as an overall ceiling, and a request has to fit under both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does "daily" reset?&lt;/strong&gt; Budget periods use UTC day boundaries, so a daily budget resets at 00:00 UTC. That's 8 PM US Eastern during daylight time and 7 PM in winter. Your organization's time zone setting changes how the console displays time, not when budgets reset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Each engineer creates their own key.&lt;/strong&gt; Each engineer signs in, opens &lt;strong&gt;API Keys → Create key&lt;/strong&gt;, picks the team, and names it something like &lt;code&gt;claude-code-alice&lt;/code&gt;. Keys a member creates are tied to that member automatically, so their member budget applies and analytics attribute spend to them. Keys an admin creates in the console belong to the team but no particular member, so only team budgets apply to those. For per-person caps, let each person create their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Add a rate limit as a safety valve.&lt;/strong&gt; The same dialog has &lt;strong&gt;RPM limit&lt;/strong&gt; and &lt;strong&gt;TPM limit&lt;/strong&gt; fields, plus an optional expiry in days. Going over a rate limit returns &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header. A budget catches the slow burn over a day. A TPM limit catches a retry loop within minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Optionally, restrict models.&lt;/strong&gt; Each team can have a model allowlist (under &lt;strong&gt;Model Access&lt;/strong&gt;, or the team's own page). A request for a model outside it fails with &lt;code&gt;403 model_not_allowed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Point Claude Code at the gateway.&lt;/strong&gt; On each engineer's machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.tokenrouter.io
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tr_your_key_here
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;/v1&lt;/code&gt; on the base URL. Claude Code appends &lt;code&gt;/v1/messages&lt;/code&gt; itself, so adding &lt;code&gt;/v1&lt;/code&gt; produces &lt;code&gt;/v1/v1/messages&lt;/code&gt; and a 404. &lt;code&gt;ANTHROPIC_API_KEY=tr_...&lt;/code&gt; also works. Put the exports in your shell profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Verify.&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;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Reply with exactly: routed via TokenRouter"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the reply comes back, you're routed through the gateway. The request shows up under &lt;strong&gt;Logs&lt;/strong&gt; within seconds, attributed to that engineer's key. Claude requests pass through verbatim, so extended thinking, tools and vision behave as they do with Anthropic directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens at the cap
&lt;/h2&gt;

&lt;p&gt;The cap is checked before the request is forwarded. The gateway reserves the request's worst-case cost: an estimate of the prompt plus the request's maximum output tokens, priced at that model's rates. If spend so far, plus other requests still in flight, plus that reservation would go over the limit, the request is refused with &lt;code&gt;429 budget_exceeded&lt;/code&gt; and never reaches Anthropic. Once the response finishes, the reservation is replaced with the actual cost, which is computed from exact token counts and the provider's published pricing.&lt;/p&gt;

&lt;p&gt;Two practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Close to the cap, large requests are refused first, while small ones may still fit.&lt;/li&gt;
&lt;li&gt;The prompt part of the reservation is an estimate. Treat the cap as a tight bound, not to-the-cent accounting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotchas to test before rolling out
&lt;/h2&gt;

&lt;p&gt;These apply to any gateway or proxy, including ours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The desktop app's Code tab currently ignores &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; from &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, while the terminal CLI honors it (anthropics/claude-code#97574).&lt;/li&gt;
&lt;li&gt;An open issue reports that any non-default base URL, even a byte-for-byte pass-through, drops message threads and sharply reduces parallel tool calls in headless runs (#98464). If parallelism matters to your workflow, measure before and after.&lt;/li&gt;
&lt;li&gt;Plugins that make their own model calls may not send custom headers (#99857).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing the number
&lt;/h2&gt;

&lt;p&gt;There's no universal right daily cap, and we won't pretend there is. A reasonable way to pick one: run a week with the hard-cap switch off (alerts only), look at each engineer's heaviest normal day, then turn the hard cap on with a limit comfortably above that. The cap's job is to stop runaway loops, not to ration normal work. Keep the TPM limit tight enough that a loop can't burn the whole day in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offboarding
&lt;/h2&gt;

&lt;p&gt;Revoke that person's keys on the API Keys page and remove them under Members. Everyone else keeps working, and nobody rotates anything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We build TokenRouter. The free plan needs no card and includes the same budgets and hard caps as paid plans. The Claude Code setup is in the docs: &lt;a href="https://www.tokenrouter.io/docs/claude-code" rel="noopener noreferrer"&gt;tokenrouter.io/docs/claude-code&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>claudecode</category>
      <category>devops</category>
    </item>
    <item>
      <title>Build AI Agents in Ruby with Just One Gem</title>
      <dc:creator>Russell Van Curen</dc:creator>
      <pubDate>Sat, 02 Aug 2025 12:35:10 +0000</pubDate>
      <link>https://dev.to/vancuren/build-ai-agents-in-ruby-with-just-one-include-2do9</link>
      <guid>https://dev.to/vancuren/build-ai-agents-in-ruby-with-just-one-include-2do9</guid>
      <description>&lt;p&gt;OpenAI’s function calling (now called &lt;code&gt;tool_calls&lt;/code&gt;) is one of the most powerful ways to extend GPT. It lets the model reason over structured actions and trigger code — giving you the foundation for real &lt;strong&gt;AI agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But for Ruby developers, using tools meant writing manual JSON schemas, messy parsing, or gluing Python into the mix.&lt;/p&gt;

&lt;p&gt;So I built this:  &lt;strong&gt;&lt;a href="https://github.com/vancuren/openai-toolable" rel="noopener noreferrer"&gt;&lt;code&gt;openai-toolable&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tiny Ruby gem to make your methods OpenAI-toolable in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;openai-toolable&lt;/code&gt;&lt;/strong&gt; gives you a simple way to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Annotate any Ruby method as a tool&lt;/li&gt;
&lt;li&gt;Auto-generate OpenAI-compatible tool schemas&lt;/li&gt;
&lt;li&gt;Handle tool calls from OpenAI response JSON&lt;/li&gt;
&lt;li&gt;Return function results cleanly — no boilerplate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s perfect for building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI agents with Ruby backends&lt;/li&gt;
&lt;li&gt;GPT-augmented Rails apps&lt;/li&gt;
&lt;li&gt;CLI tools, Discord bots, copilots — you name it&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"openai/toolable"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"openai"&lt;/span&gt;

&lt;span class="c1"&gt;# Set your API key&lt;/span&gt;
&lt;span class="no"&gt;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Define a tool with required parameters&lt;/span&gt;
&lt;span class="n"&gt;weather_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Openai&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Toolable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ToolFactory&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="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"get_weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Get the current weather in a given location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;parameters: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"The city and state, e.g. San Francisco, CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"The unit of temperature, e.g. celsius or fahrenheit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&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;# Create a tool handler and register the tool&lt;/span&gt;
&lt;span class="n"&gt;tool_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Openai&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Toolable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ToolHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;tool_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"get_weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;lambda: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Getting the weather in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Now you can generate the tool spec and call the tool from an OpenAI response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a client&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenAI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&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="ss"&gt;api_key: &lt;/span&gt;&lt;span class="no"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="c1"&gt;# Create a chat completion&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"What's the weather like in Boston?"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;weather_tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;tool_choice: &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Handle the response&lt;/span&gt;
  &lt;span class="n"&gt;tool_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;response: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"An error occurred: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a full round-trip:&lt;br&gt;
Tool spec → GPT picks it → You call it → Return result.&lt;/p&gt;


&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Define tools inline with your Ruby logic&lt;/li&gt;
&lt;li&gt;Fully OpenAI-compatible tool schemas&lt;/li&gt;
&lt;li&gt;Works with any GPT-4 / gpt-3.5 &lt;code&gt;tool_calls&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Clean, declarative DSL&lt;/li&gt;
&lt;li&gt;Supports multiple tools per class&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Add it to your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"openai-toolable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;openai-toolable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Full Docs &amp;amp; Usage
&lt;/h2&gt;

&lt;p&gt;All source code, examples, and docs here: &lt;a href="https://github.com/vancuren/openai-toolable" rel="noopener noreferrer"&gt;https://github.com/vancuren/openai-toolable&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use This?
&lt;/h2&gt;

&lt;p&gt;With OpenAI’s &lt;code&gt;tool_calls&lt;/code&gt;, the future is &lt;strong&gt;function-aware models&lt;/strong&gt;.&lt;br&gt;
This gem lets you build GPT agents using &lt;strong&gt;just Ruby&lt;/strong&gt;, without the mess of wiring schemas and handlers manually.&lt;/p&gt;

&lt;p&gt;Instead of this:&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;"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;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"function"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"calculate_something"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&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="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;You just write a method and mark it &lt;code&gt;tool(...)&lt;/code&gt;.&lt;br&gt;
Simple, readable, and ready to go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Ideas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tool chaining for multi-step calls&lt;/li&gt;
&lt;li&gt;Streaming + parallel calls&lt;/li&gt;
&lt;li&gt;Built-in error handling + retry&lt;/li&gt;
&lt;li&gt;Optional Rails generators for service objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open to feature suggestions! Drop them in &lt;a href="https://github.com/vancuren/openai-toolable/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt; or send a PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Help Spread the Word
&lt;/h2&gt;

&lt;p&gt;If you find this useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ Star the repo&lt;/li&gt;
&lt;li&gt;Share it on Twitter/X&lt;/li&gt;
&lt;li&gt;Mention it in &lt;code&gt;r/ruby&lt;/code&gt; or dev communities&lt;/li&gt;
&lt;li&gt;Link it in your AI projects or Rails agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s make Ruby a first-class citizen in the AI tooling world &lt;/p&gt;




&lt;h2&gt;
  
  
  Stay in the Loop
&lt;/h2&gt;

&lt;p&gt;I'm building open-source AI infrastructure and Ruby dev tools.&lt;br&gt;
Follow me on &lt;a href="https://github.com/vancuren" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for updates.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>openai</category>
      <category>rails</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
