<?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: Paw from Oz</title>
    <description>The latest articles on DEV Community by Paw from Oz (@pawfromoz).</description>
    <link>https://dev.to/pawfromoz</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%2F4036916%2F7d8c7b46-1060-440d-bfb5-0f1daf99ff58.png</url>
      <title>DEV Community: Paw from Oz</title>
      <link>https://dev.to/pawfromoz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawfromoz"/>
    <language>en</language>
    <item>
      <title>A small CLI to stop hitting Claude Code's "too many MCP tools loaded" warning</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:06:01 +0000</pubDate>
      <link>https://dev.to/pawfromoz/a-small-cli-to-stop-hitting-claude-codes-too-many-mcp-tools-loaded-warning-1e3o</link>
      <guid>https://dev.to/pawfromoz/a-small-cli-to-stop-hitting-claude-codes-too-many-mcp-tools-loaded-warning-1e3o</guid>
      <description>&lt;p&gt;If you run more than a couple of MCP servers in Claude Code, you've probably hit&lt;br&gt;
the loaded-tools warning (~40 tools). The usual fix is manually enabling/disabling&lt;br&gt;
servers by hand every time your task changes, and remembering which tools depend&lt;br&gt;
on which.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp-profile&lt;/code&gt; is a small, dependency-free CLI (+ a Claude Code skill) that lets&lt;br&gt;
you define named profiles — e.g. "research", "coding", "deploy" — each listing&lt;br&gt;
the MCP servers that task actually needs, then switch between them with one&lt;br&gt;
command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-profile use research
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It edits the same &lt;code&gt;enabledMcpjsonServers&lt;/code&gt; / &lt;code&gt;disabledMcpjsonServers&lt;/code&gt; fields&lt;br&gt;
Claude Code already reads — no new config format, no daemon, no account, no&lt;br&gt;
network calls. MIT licensed.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/pozga/mcp-profile" rel="noopener noreferrer"&gt;https://github.com/pozga/mcp-profile&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback wanted: does this match how you're actually hitting the tool-count&lt;br&gt;
problem? What would make this worth paying for (team-shared profiles? a&lt;br&gt;
dashboard?) vs. just a free utility?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; Thanks &lt;a href="https://dev.to/alexshev"&gt;@alexshev&lt;/a&gt; for the comment below —&lt;br&gt;
shipped exactly what you asked for. The default profile example now ships five&lt;br&gt;
task-type profiles out of the box: &lt;code&gt;coding&lt;/code&gt;, &lt;code&gt;research&lt;/code&gt;, &lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;.&lt;br&gt;
See the &lt;a href="https://github.com/pozga/mcp-profile/commit/52c8b7c" rel="noopener noreferrer"&gt;v0.2 commit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I caught a Claude prompt regression before it hit production</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:24:41 +0000</pubDate>
      <link>https://dev.to/pawfromoz/how-i-caught-a-claude-prompt-regression-before-it-hit-production-4bmd</link>
      <guid>https://dev.to/pawfromoz/how-i-caught-a-claude-prompt-regression-before-it-hit-production-4bmd</guid>
      <description>&lt;p&gt;I changed one line in my system prompt last week. A single word — "concisely" became "briefly". I thought nothing of it.&lt;/p&gt;

&lt;p&gt;Two hours later, a user complained that the support bot was cutting them off mid-explanation.&lt;/p&gt;

&lt;p&gt;I had no tests. I had no diff. I had no idea what changed.&lt;/p&gt;

&lt;p&gt;That's what pushed me to build &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt; — a testing framework for AI agents. This is how I actually use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with testing AI agents
&lt;/h2&gt;

&lt;p&gt;Traditional testing is deterministic: input X always produces output Y. AI agents don't work that way. Ask the same question twice and you get slightly different answers. That's not a bug — it's the whole point.&lt;/p&gt;

&lt;p&gt;So you can't do &lt;code&gt;expect(response).toBe("Reset your password by...")&lt;/code&gt;. You need flexible assertions that check &lt;em&gt;intent&lt;/em&gt;, not exact output.&lt;/p&gt;

&lt;p&gt;AgentSpec gives you that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up your first test
&lt;/h2&gt;

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

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

&lt;/div&gt;



&lt;p&gt;AgentSpec tests any HTTP agent that accepts &lt;code&gt;{input: "..."}&lt;/code&gt; and returns &lt;code&gt;{output: "..."}&lt;/code&gt;. Wrap your existing agent in one route and you're done.&lt;/p&gt;

&lt;p&gt;Create a test file &lt;code&gt;tests/support-agent.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;regression&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;suite"&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;not_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;don't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;know"&lt;/span&gt;
      &lt;span class="na"&gt;max_latency_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;billing&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;correct&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;team"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;want&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;refund&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;month"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connect"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;tool_called&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer_to_billing"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doesn't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hallucinate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;details"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;not_contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;60&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;90&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; https://your-agent.example.com/chat &lt;span class="nt"&gt;--dir&lt;/span&gt; tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The behavior diff — where the real value is
&lt;/h2&gt;

&lt;p&gt;After the first passing run, AgentSpec stores the baseline output for each test. When something changes, you see exactly what shifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING REGRESSION support agent &amp;gt; handles password reset
  Behavior REGRESSED — test was passing, now failing
  + added: briefly, concise, short
  - removed: step, by, step, here, is, how
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the word diff between what the agent used to say and what it says now. In my case, adding "briefly" to the system prompt made the agent stop giving step-by-step instructions — which users actually wanted.&lt;/p&gt;

&lt;p&gt;This diff is what I didn't have before. It would have caught the regression in 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM-as-judge for semantic checks
&lt;/h2&gt;

&lt;p&gt;Some things are hard to check with string matching. "Is this response helpful?" isn't a regex problem.&lt;/p&gt;

&lt;p&gt;AgentSpec supports LLM-as-judge using a local Ollama model — no API costs, runs in CI:&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;# Start Ollama locally&lt;/span&gt;
ollama pull qwen2.5:7b

&lt;span class="c"&gt;# Run with judge&lt;/span&gt;
agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; https://your-agent.example.com/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--judge-endpoint&lt;/span&gt; http://127.0.0.1:11434/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--judge-model&lt;/span&gt; qwen2.5:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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;empathetic"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I've&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;been&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;waiting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;llm_judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;acknowledge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frustration&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;offer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;help,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dismiss&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;complaint"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doesn't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;upsell&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&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;upset"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;broke&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;day"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;llm_judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Focus&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;resolving&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;promotional&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;upsell&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;attempts"&lt;/span&gt;
      &lt;span class="na"&gt;not_contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;premium"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offer"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CI integration
&lt;/h2&gt;

&lt;p&gt;Add this to your GitHub Actions workflow and you'll catch regressions before they merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test agent behavior&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npm install -g @ozperium/agentspec&lt;/span&gt;
    &lt;span class="s"&gt;agentspec run --endpoint ${{ secrets.AGENT_URL }} --dir tests --ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--ci&lt;/code&gt; mode exits with code 1 on any failure. Combine it with branch protection and no prompt change ships without passing the test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start small
&lt;/h2&gt;

&lt;p&gt;You don't need 50 tests. Start with three scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The happy path&lt;/strong&gt; — does it handle the most common request correctly?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The edge case that burned you before&lt;/strong&gt; — the thing that went wrong last time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The thing you'd be embarrassed by&lt;/strong&gt; — hallucinated facts, wrong policy details, off-brand tone&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three tests, 15 minutes to write, run on every deploy. That's the baseline.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/agentspec
agentspec init
agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; https://your-agent.example.com/chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;github.com/Ozperium/agentspec&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop manually logging your AI API calls — one-line auto-logging for OpenAI and Anthropic</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Sat, 25 Jul 2026 04:23:00 +0000</pubDate>
      <link>https://dev.to/pawfromoz/stop-manually-logging-your-ai-api-calls-one-line-auto-logging-for-openai-and-anthropic-1b5m</link>
      <guid>https://dev.to/pawfromoz/stop-manually-logging-your-ai-api-calls-one-line-auto-logging-for-openai-and-anthropic-1b5m</guid>
      <description>&lt;p&gt;Every time you call an AI API, you're spending money. But unless you're checking the dashboard after each session, you have no idea which project or model is driving the bill.&lt;/p&gt;

&lt;p&gt;I solved this by wrapping my AI clients with a one-line middleware that logs every call automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with manual logging
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;AICostTracker&lt;/a&gt; lets you run &lt;code&gt;aicost log myapp gpt-4o 1500 800&lt;/code&gt; to record a call. Useful — but nobody does this consistently. You forget, you're in flow, you'll do it later.&lt;/p&gt;

&lt;p&gt;The logs end up patchy and the summary is useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-logging with &lt;code&gt;track()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In v0.1.2, I added a &lt;code&gt;track()&lt;/code&gt; function that wraps your OpenAI or Anthropic client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ozperium/aicost-tracker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ozperium/aicost-tracker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myapp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// From here, every call is logged automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize this doc&lt;/span&gt;&lt;span class="dl"&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;That's it. No changes to your existing code beyond the wrap.&lt;/p&gt;

&lt;p&gt;It works the same way with Anthropic:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ozperium/aicost-tracker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-3-5-sonnet-20241022&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What gets logged
&lt;/h2&gt;

&lt;p&gt;After a few sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aicost summary
&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;  AI Cost Tracker — Summary
  ══════════════════════════════════════════════════
  Total cost:       $1.2847
  Input tokens:     124,000
  Output tokens:    67,500

  By Project:
  ──────────────────────────────────────────────────
  myapp                $    0.9102  31 calls
  summarizer           $    0.3745  12 calls

  By Model:
  ──────────────────────────────────────────────────
  gpt-4o               $    0.9102  31 calls
  claude-3-5-sonnet    $    0.3745  12 calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;track()&lt;/code&gt; patches &lt;code&gt;client.chat.completions.create&lt;/code&gt; (OpenAI) or &lt;code&gt;client.messages.create&lt;/code&gt; (Anthropic) to intercept the response, extract usage from the &lt;code&gt;usage&lt;/code&gt; field, and call &lt;code&gt;logUsage()&lt;/code&gt; before returning. It's a thin synchronous wrapper — zero added latency.&lt;/p&gt;

&lt;p&gt;If logging fails for any reason, the error is swallowed silently. Your API call always gets its response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-project breakdown
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;{ project: 'myapp' }&lt;/code&gt; option lets you tag every call with a project name. If you're running multiple agents or tools in the same codebase, give each its own project tag:&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;researchAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summaryAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;aicost summary&lt;/code&gt; shows the breakdown by project automatically.&lt;/p&gt;




&lt;p&gt;GitHub: &lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;https://github.com/Ozperium/aicost-tracker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also: &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt; for testing AI agent behavior, &lt;a href="https://github.com/Ozperium/quota" rel="noopener noreferrer"&gt;quota&lt;/a&gt; for monitoring rate limits.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Testing AI agents is hard. I built a framework for it.</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Fri, 24 Jul 2026 04:22:54 +0000</pubDate>
      <link>https://dev.to/pawfromoz/testing-ai-agents-is-hard-i-built-a-framework-for-it-3hk0</link>
      <guid>https://dev.to/pawfromoz/testing-ai-agents-is-hard-i-built-a-framework-for-it-3hk0</guid>
      <description>&lt;p&gt;Your AI agent works in dev. You change a prompt to improve tone. Now it stops routing billing questions correctly.&lt;/p&gt;

&lt;p&gt;You don't find out until a user complains.&lt;/p&gt;

&lt;p&gt;The problem: AI agents are non-deterministic. Traditional unit tests don't work. &lt;code&gt;expect(output).toBe("transfer to billing")&lt;/code&gt; fails 30% of the time on correct behavior, and passes when the agent is broken in subtle ways.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt; to fix this — a testing framework designed specifically for non-deterministic AI output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;Define tests in YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-agent-tests"&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;question"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;want&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;refund"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;tool_called&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer_to_billing"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
      &lt;span class="na"&gt;not_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;don't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;know"&lt;/span&gt;
      &lt;span class="na"&gt;max_latency_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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;on-topic"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;semantically_similar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credentials"&lt;/span&gt;
      &lt;span class="na"&gt;min_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/agentspec
agentspec init
agentspec run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AgentSpec v1.2.0

  ✓ routes billing question (312ms)
  ✓ handles expired token (891ms)
  ✗ response is on-topic
    Expected semantic similarity ≥ 0.5, got 0.31
    Input: "how do I reset my password?"
    Output: "Please contact support for account issues."

2 passed, 1 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why assertions built for AI
&lt;/h2&gt;

&lt;p&gt;Standard test assertions assume deterministic output. AI output isn't.&lt;/p&gt;

&lt;p&gt;AgentSpec provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;contains&lt;/code&gt; / &lt;code&gt;not_contains&lt;/code&gt;&lt;/strong&gt; — substring presence (handles paraphrasing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;contains_any&lt;/code&gt; / &lt;code&gt;contains_all&lt;/code&gt;&lt;/strong&gt; — flexible multi-keyword matching
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;semantically_similar&lt;/code&gt;&lt;/strong&gt; — word-overlap similarity score, not exact match&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;regex&lt;/code&gt;&lt;/strong&gt; — pattern matching for structured output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_called&lt;/code&gt;&lt;/strong&gt; — verify the agent invoked the right tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_latency_ms&lt;/code&gt;&lt;/strong&gt; — catch regressions in response time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm_judge&lt;/code&gt;&lt;/strong&gt; — use a local model (Ollama) to evaluate quality in natural language&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CI integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--ci&lt;/span&gt;   &lt;span class="c"&gt;# exits 1 on failure, JUnit XML output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run AgentSpec&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentspec run --ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every PR that changes a prompt, model, or tool gets behavior regression tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavior diff reports
&lt;/h2&gt;

&lt;p&gt;Run with &lt;code&gt;--diff&lt;/code&gt; to compare against a previous baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--diff&lt;/span&gt; baseline.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows exactly what changed between runs — useful when you swap models or update prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP agents
&lt;/h2&gt;

&lt;p&gt;Point it at any running agent endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; http://localhost:3000/chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No SDK integration required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/agentspec
agentspec init
agentspec run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;https://github.com/Ozperium/agentspec&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also in the stack: &lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;AICostTracker&lt;/a&gt; for tracking what you spend on AI APIs, and &lt;a href="https://github.com/Ozperium/quota" rel="noopener noreferrer"&gt;quota&lt;/a&gt; for monitoring rate limits before they stop you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>devtools</category>
      <category>llm</category>
    </item>
    <item>
      <title>I built a terminal tool to see your AI coding limits before they stop you</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 22:23:44 +0000</pubDate>
      <link>https://dev.to/pawfromoz/i-built-a-terminal-tool-to-see-your-ai-coding-limits-before-they-stop-you-3a93</link>
      <guid>https://dev.to/pawfromoz/i-built-a-terminal-tool-to-see-your-ai-coding-limits-before-they-stop-you-3a93</guid>
      <description>&lt;p&gt;The worst moment in a coding session is hitting a rate limit mid-task.&lt;/p&gt;

&lt;p&gt;You're deep in a refactor. Claude Code stops. Or Codex resets. And you spend the next 20 minutes switching tools, losing context, or just waiting. The "Codex Resets" thread on Hacker News got 270 points and 175 comments. The pain is widespread.&lt;/p&gt;

&lt;p&gt;The problem isn't the limit — it's that you don't know how close you are until you hit it.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Ozperium/quota" rel="noopener noreferrer"&gt;quota&lt;/a&gt; to fix that.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quota
&lt;span class="go"&gt;
  quota — your ai usage, at a glance

  Claude Code   ████████████░░  3h 12m of 5h    reset in 2h 48m
  Codex         ███░░░░░░░░░░░  3 of 40 reqs    reset in 4h 12m
  Cursor        ──────────────  not connected
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Shows remaining usage for every AI coding tool you have connected, along with reset times.&lt;/p&gt;

&lt;p&gt;You can also run it in watch mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quota watch 30   &lt;span class="c"&gt;# re-check every 30 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pipe it into scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quota &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.providers.claude_code.percent_remaining'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;quota&lt;/code&gt; reads provider state &lt;strong&gt;locally&lt;/strong&gt; — no API calls, no accounts, no telemetry. It reads from &lt;code&gt;~/.claude&lt;/code&gt; and &lt;code&gt;~/.codex&lt;/code&gt; directly, the same files the tools themselves write. Everything stays on your machine.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not just check the app?
&lt;/h2&gt;

&lt;p&gt;Claude Code and Codex don't surface remaining quota prominently. You either have to open a browser dashboard or wait for the error. Neither is useful when you're mid-task in a terminal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;quota&lt;/code&gt; lives where you work — in the terminal, right next to your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OAuth login for Cursor and Grok (local reads aren't possible for these)&lt;/li&gt;
&lt;li&gt;Shell prompt integration (show remaining % in starship/oh-my-posh)&lt;/li&gt;
&lt;li&gt;CI check — fail a build if you're out of quota before a long test run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Ozperium/quota" rel="noopener noreferrer"&gt;https://github.com/Ozperium/quota&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/@ozperium/quota" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@ozperium/quota&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also in the stack: &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt; for testing AI agent behavior, and &lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;AICostTracker&lt;/a&gt; for tracking what you're spending on AI APIs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a local-first CLI to track what I'm actually spending on AI APIs</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 22:23:17 +0000</pubDate>
      <link>https://dev.to/pawfromoz/i-built-a-local-first-cli-to-track-what-im-actually-spending-on-ai-apis-2ajo</link>
      <guid>https://dev.to/pawfromoz/i-built-a-local-first-cli-to-track-what-im-actually-spending-on-ai-apis-2ajo</guid>
      <description>&lt;p&gt;The worst part of using AI APIs isn't the cost. It's not knowing what you're spending until the bill arrives.&lt;/p&gt;

&lt;p&gt;I wanted per-project cost tracking — which project used the most tokens? Which model was the expensive one? The cloud dashboards don't tell you that.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;AICostTracker&lt;/a&gt; — a local CLI that logs every API call and breaks down costs by project and model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-logging (the easy way)
&lt;/h2&gt;

&lt;p&gt;Install as a library and wrap your AI client — every call is logged automatically, no manual work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ozperium/aicost-tracker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ozperium/aicost-tracker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myapp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Nothing else to do — all calls now logged&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&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;Works with Anthropic too:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ozperium/aicost-tracker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docs-bot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// All messages.create calls now logged automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper is transparent — it never surfaces logging errors to your code, and adds zero latency to the API call itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  See your costs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aicost summary
&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;  AI Cost Tracker — Summary
  ══════════════════════════════════════════════════
  Total entries:    12
  Total cost:       $0.4821
  Input tokens:     48,000
  Output tokens:    31,200

  By Project:
  ──────────────────────────────────────────────────
  myapp                $    0.3102  8 calls
  docs-bot             $    0.1719  4 calls

  By Model:
  ──────────────────────────────────────────────────
  gpt-4o               $    0.3102  8 calls
  claude-3-5-sonnet    $    0.1719  4 calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Manual logging (CLI)
&lt;/h2&gt;

&lt;p&gt;If you prefer not to install as a library, you can log calls manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/aicost-tracker
aicost log myapp gpt-4o 1500 800 &lt;span class="s2"&gt;"generated README"&lt;/span&gt;
aicost summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why local-first?
&lt;/h2&gt;

&lt;p&gt;All data lives in &lt;code&gt;~/.aicost/usage.jsonl&lt;/code&gt; — one JSON line per call, greppable, portable, yours. No cloud account, no telemetry, no dashboard to maintain.&lt;/p&gt;

&lt;p&gt;Pricing is baked in for OpenAI (gpt-4o, o1, gpt-4-turbo, gpt-3.5), Anthropic (claude-3.5-sonnet, claude-3-opus, haiku), Google (gemini-1.5), and local/Ollama (cost: $0).&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Ozperium/aicost-tracker" rel="noopener noreferrer"&gt;https://github.com/Ozperium/aicost-tracker&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also in the stack: &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt; for testing AI agent behavior, and &lt;a href="https://github.com/Ozperium/quota" rel="noopener noreferrer"&gt;quota&lt;/a&gt; for monitoring rate limits before they stop you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stoke: A kill switch for runaway AI agents</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:00:58 +0000</pubDate>
      <link>https://dev.to/pawfromoz/stoke-a-kill-switch-for-runaway-ai-agents-p0k</link>
      <guid>https://dev.to/pawfromoz/stoke-a-kill-switch-for-runaway-ai-agents-p0k</guid>
      <description>&lt;p&gt;I've been building with AI coding agents daily — Claude Code, OpenCode, custom agents. They're productive, but they have a failure mode that costs real money: they retry, loop, and fan out on metered API keys. Billing alerts fire hours after the damage is done.&lt;/p&gt;

&lt;p&gt;Observability tools (Langfuse, Helicone, Portkey) tell you what happened. None of them &lt;strong&gt;stop it from happening&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stoke
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://stokegate.com" rel="noopener noreferrer"&gt;Stoke&lt;/a&gt; is a single Rust binary (~5.5 MB, zero runtime dependencies) that sits between your AI agents and model providers. It enforces policy &lt;strong&gt;before&lt;/strong&gt; any provider is called:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard budget caps&lt;/strong&gt; — per-API-key spend limits in USD. Over the limit = 429 Budget exceeded, not an alert. Streamed responses accrue spend too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop detection&lt;/strong&gt; — exact prompt-hash matching plus optional semantic similarity. 5 similar requests from one key in 60 seconds = blocked for 120 seconds. Catches agents stuck in retry loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — per-key requests-per-minute over a sliding window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: &lt;strong&gt;enforce, don't just observe.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What else it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node-aware routing&lt;/strong&gt; — polls your Ollama nodes, places requests on warm machines, balances by live load, fails over automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federation&lt;/strong&gt; — one Stoke can front another across machines, with a hop guard that prevents cycles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-routing&lt;/strong&gt; — send &lt;code&gt;"model": "auto"&lt;/code&gt; and Stoke scores every (model, node) candidate on cost, predicted latency, and your preferences. Every response carries a receipt explaining the decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Messages API&lt;/strong&gt; — point Claude Code at Stoke with &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. Same enforcement, then passthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; — built-in PII redaction, audit logging, prompt harness + HTTP webhooks in any language + JS/TS via deno_core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed auth&lt;/strong&gt; — no keys configured and no dev flag = every request rejected. There is no accidentally-open state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;Every Python proxy (LiteLLM, OpenRouter) adds latency and memory overhead — 500MB+ RAM, Postgres, Redis. Stoke is 5.5 MB, starts in milliseconds, uses ~10 MB RAM, and has zero runtime dependencies. It's a single binary you can curl-pipe-sh install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-machine setup
&lt;/h2&gt;

&lt;p&gt;If you have Ollama running on two machines (say a MacBook and a Mac Studio), Stoke routes between them automatically — preferring the machine where the model is already loaded in RAM, balancing by in-flight count, and measuring real time-to-first-token per node.&lt;/p&gt;

&lt;p&gt;Or use federation: run a Stoke on each machine, keep Ollama on 127.0.0.1, and front them with a &lt;code&gt;type = "stoke"&lt;/code&gt; provider. Stoke is the only network-facing door, and it requires auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://stokegate.com/install | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal config:&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;routing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"single"&lt;/span&gt;
&lt;span class="py"&gt;default_model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"qwen3:8b"&lt;/span&gt;

&lt;span class="nn"&gt;[server]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8787&lt;/span&gt;

&lt;span class="nn"&gt;[[providers]]&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;"local"&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;"http://127.0.0.1:11434/v1"&lt;/span&gt;
&lt;span class="py"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STOKE_API_KEYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;stk-mykey ./target/release/stoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point any OpenAI-compatible client at &lt;code&gt;http://localhost:8787/v1&lt;/code&gt; — OpenCode, Cursor, custom apps. Budget caps, rate limits, and loop detection apply to every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;Pre-release, MIT licensed, EU-based. Dogfooded daily on a real two-Mac + Ollama setup. Built in the open at &lt;a href="https://github.com/Ozperium/stoke" rel="noopener noreferrer"&gt;github.com/Ozperium/stoke&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The smoke scripts (&lt;code&gt;./scripts/smoke.sh&lt;/code&gt;, &lt;code&gt;./scripts/smoke_federation.sh&lt;/code&gt;) run the full system end-to-end with mock Ollama nodes — no Ollama required to verify the claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic to OpenAI translation (so Claude Code can run against local models)&lt;/li&gt;
&lt;li&gt;Homebrew formula and Docker images&lt;/li&gt;
&lt;li&gt;Per-key loop-detection thresholds&lt;/li&gt;
&lt;li&gt;OpenTelemetry traces and Prometheus metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback welcome. The repo is &lt;a href="https://github.com/Ozperium/stoke" rel="noopener noreferrer"&gt;github.com/Ozperium/stoke&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>rust</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Testing AI Agents with AgentSpec: Jest for Non-Deterministic Behavior</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:13:47 +0000</pubDate>
      <link>https://dev.to/pawfromoz/testing-ai-agents-with-agentspec-jest-for-non-deterministic-behavior-55kg</link>
      <guid>https://dev.to/pawfromoz/testing-ai-agents-with-agentspec-jest-for-non-deterministic-behavior-55kg</guid>
      <description>&lt;p&gt;AI agents are non-deterministic. This is both their superpower and their biggest testing challenge.&lt;/p&gt;

&lt;p&gt;When you change a prompt, swap a model, or update a tool definition, the agent's behavior shifts in ways you can't predict. Traditional test tools (Jest, Vitest, Playwright) are built for deterministic code — they expect exact string matching. AI agents don't work that way.&lt;/p&gt;

&lt;p&gt;Last week, I changed a system prompt in a support agent. It still passed all our Jest tests (the function still returned a string). But the agent had stopped offering password reset help — a regression that only surfaced when a user complained.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AgentSpec?
&lt;/h2&gt;

&lt;p&gt;AgentSpec is a testing framework designed specifically for AI agents. It provides assertions that handle non-deterministic output, behavior diff reports that show what changed, and CI/CD integration out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions for AI output
&lt;/h2&gt;

&lt;p&gt;Instead of exact string matching, AgentSpec provides assertions that work with the variability of AI responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# agentspec.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent"&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
      &lt;span class="na"&gt;not_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;don't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;know"&lt;/span&gt;
      &lt;span class="na"&gt;max_latency_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;question"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;want&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;refund"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;team"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;tool_called&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer_to_billing"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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;on-topic"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;semantically_similar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credentials"&lt;/span&gt;
      &lt;span class="na"&gt;min_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Available assertions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;contains / not_contains&lt;/strong&gt; — substring checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;contains_any / contains_all&lt;/strong&gt; — flexible multi-string matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;regex&lt;/strong&gt; — pattern matching (error codes, IDs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tool_called&lt;/strong&gt; — verify specific tools were invoked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;max_latency_ms / max_tokens&lt;/strong&gt; — performance and cost gates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;semantically_similar&lt;/strong&gt; — word-overlap similarity (no API needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;json_path&lt;/strong&gt; — extract and verify fields from JSON agent output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llm_judge&lt;/strong&gt; — use a local LLM to evaluate output quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Behavior diff reports
&lt;/h2&gt;

&lt;p&gt;This is the killer feature. AgentSpec stores the last passing output per test. When behavior changes, you see exactly what shifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ REGRESSION support agent &amp;gt; handles expired token
  ⚠  Behavior REGRESSED — test was passing, now failing
  + added: your, token, has, expired, please, contact, support
  - removed: you, need, refresh, the, token, settings, security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of just knowing a test failed, you know WHAT changed. This turns debugging from "what broke?" into "the model stopped saying 'refresh token' and started saying 'renew credentials'."&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM-as-judge with local models
&lt;/h2&gt;

&lt;p&gt;The most powerful assertion is &lt;code&gt;llm_judge&lt;/code&gt; — use an LLM to evaluate whether a response meets quality criteria that can't be expressed as string matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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;helpful"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;llm_judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;explain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;how&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;reset&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;password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, AgentSpec uses a local Ollama model as the judge — no API costs, no data leaving your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--judge-endpoint&lt;/span&gt; http://127.0.0.1:11434/v1/chat/completions &lt;span class="nt"&gt;--judge-model&lt;/span&gt; qwen2.5:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing real agents
&lt;/h2&gt;

&lt;p&gt;AgentSpec includes an HTTP agent adapter to test any HTTP-accessible AI agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; https://my-agent.example.com/chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It POSTs &lt;code&gt;{input: "..."}&lt;/code&gt; to your endpoint and reads the response. Works with any agent that exposes an HTTP API.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD integration
&lt;/h2&gt;

&lt;p&gt;AgentSpec is designed to drop into any CI pipeline:&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;# Exit code 1 on failure&lt;/span&gt;
agentspec run &lt;span class="nt"&gt;--ci&lt;/span&gt;

&lt;span class="c"&gt;# JUnit XML for CI systems&lt;/span&gt;
agentspec run &lt;span class="nt"&gt;--junit&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentspec/action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/agentspec
agentspec init
agentspec run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. YAML test definitions, assertions for non-deterministic output, diff reports, CI integration. No cloud, no account, no telemetry.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;Ozperium/agentspec&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing:&lt;/strong&gt; &lt;a href="https://agentspec.pages.dev" rel="noopener noreferrer"&gt;agentspec.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love feedback on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is YAML the right format, or would you prefer TS test definitions?&lt;/li&gt;
&lt;li&gt;What assertions are you missing for your AI agents?&lt;/li&gt;
&lt;li&gt;Would you use LLM-as-judge with a local model?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>agents</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
