DEV Community

bayuh85
bayuh85

Posted on

grunt-proxy: a local proxy that trims your coding agent's prompts (and shows what it saved)

Repo: πŸ‘‰ github.com/bayuh85/grunt-proxy (MIT, Node.js 20+, TypeScript)

The problem

If you use Claude Code, Cursor, or Cline every day, you are sending a lot of text you didn't write: system prompts, tool descriptions, tool results, and your own "could you please…" phrasing. Every token of that is billed. And the replies often come back with a greeting, a recap, and a closing paragraph you'll never read.

None of that is a big deal per request. Across a day of agent loops, it adds up, and most of us have no idea by how much.

I wanted something that sits between the agent and the API, trims the obvious padding, asks the model to get to the point, and tells me what it actually saved β€” without breaking code or tool calls.

What grunt-proxy does

grunt-proxy is a small local HTTP proxy for coding agents. For each request it:

  1. Shortens the prompt you were about to send (filler phrases, articles, common short forms β€” configurable).
  2. Asks the model for a terse reply via a short, stable instruction.
  3. Prints how many tokens and how many dollars that saved.

What it deliberately does not do:

  • Replies stay in normal English. The proxy doesn't role-play and doesn't rewrite the model's response.
  • Code, JSON, and tool calls stay intact.

It speaks both the OpenAI Chat Completions API and the Anthropic Messages API (including the Anthropic SSE stream), so the same proxy works for Cursor, Cline, and Claude Code.

Quickstart

Requires Node.js 20 or newer.

git clone https://github.com/bayuh85/grunt-proxy.git
cd grunt-proxy
cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Put your upstream key in GRUNT_API_KEY in .env. The default upstream is AtmoRouter (keys look like ar-...), but you can point it anywhere β€” more on that below.

npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

grunt-proxy now listens on http://127.0.0.1:8642. Try a request through it:

curl http://127.0.0.1:8642/v1/chat/completions \
  -H "content-type: application/json" \
  -H "authorization: Bearer $GRUNT_API_KEY" \
  -d '{
    "model": "atmo/deepseek-v4.1-flash",
    "messages": [
      {"role": "user", "content": "Could you please explain the configuration options in the documentation? Thank you!"}
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

(Model ids change β€” check your provider's current catalog before relying on an example id.)

Each request prints one line on stderr:

grunt  atmo/deepseek-v4.1-flash  in 1,204β†’640 (βˆ’46.8%)  out 180  saved $0.001692
Enter fullscreen mode Exit fullscreen mode

For the running total, hit GET /savings.txt or press Ctrl+C when the server is in the foreground:

grunt-proxy savings
requests              12
failed                0
input tokens          48,200 β†’ 21,040  (βˆ’56.3%)
input tokens saved    27,160
output tokens         6,410
est. input $ saved    $0.081480
projected output $    $0.000000
est. $ saved          $0.081480
upstream billed       –
Enter fullscreen mode Exit fullscreen mode

To be clear, the README labels those numbers as an illustration of the layout, not a benchmark. Your savings depend on your prompts, your level setting, and your prices.

For a compiled build: npm run build && npm start.

Setting it up with your agent

Claude Code

Claude Code (and the Anthropic SDK) appends /v1/messages itself, so use the proxy origin without /v1:

export ANTHROPIC_BASE_URL="http://127.0.0.1:8642"
export ANTHROPIC_API_KEY="ar-your-key"
claude
Enter fullscreen mode Exit fullscreen mode

grunt-proxy receives POST /v1/messages, compresses it, and forwards it upstream, streaming included. It sets anthropic-version: 2023-06-01 if the client didn't send one.

Cursor

  1. Start grunt-proxy (npm run dev).
  2. Open Cursor Settings β†’ Models.
  3. Set the OpenAI API key to your upstream key. If GRUNT_API_KEY is already set in .env, any non-empty placeholder works β€” the proxy replaces the client key.
  4. Override the OpenAI base URL with http://127.0.0.1:8642/v1.
  5. Add a custom model whose id matches a model on your upstream, e.g. atmo/deepseek-v4.1-flash.

Cline

  1. Start grunt-proxy.
  2. Set the API provider to OpenAI Compatible.
  3. Base URL: http://127.0.0.1:8642/v1
  4. API key: your upstream key (or a placeholder when GRUNT_API_KEY is set).
  5. Model ID: a catalog id from your provider.

Quick reference:

Client Base URL the client uses
Cursor, Cline, OpenAI SDKs http://127.0.0.1:8642/v1
Claude Code, Anthropic SDK http://127.0.0.1:8642

How it works

What gets compressed (and what doesn't)

grunt-proxy only edits text the model will read: system and user text, assistant text, tool descriptions, and tool-result text. It leaves these alone:

  • fenced code, inline code, and indented code lines
  • URLs, file paths, and filenames
  • JSON values, including tool-call arguments and tool parameter schemas
  • camelCase and PascalCase identifiers
  • image blocks and Anthropic tool_use / thinking blocks

Levels

Level What changes
0 Nothing.
1 Courtesy and padded phrases ("could you please", "in order to", "thank you").
2 Level 1, plus a/an/the and a few other fillers, plus short forms like config, fn, param, impl.
3 Level 2, plus of/that, some contractions, and shorter forms like cfg, btwn, w/o.

Level 2 is the default. If an agent starts misreading instructions, drop to level 1. Level 3 is lossier; only use it once you've checked the output.

You can override per request with headers: x-grunt: off forwards the body unchanged, and x-grunt-level: 0–3 / x-grunt-terse: auto|always|off override the config for one call.

The terse instruction

With GRUNT_TERSE=auto (the default), the proxy adds a one-paragraph instruction only when compression already pays for it, or when the prompt is roughly 800+ estimated tokens. It tells the model to answer in normal English, skip the greeting and recap, and keep code and tool calls valid.

A nice detail: the instruction is the same bytes for a given level, so a provider's prompt cache can still reuse the prefix.

Honest savings math

This is the part I appreciate most. The README is upfront about what's measured and what's estimated:

  • The token delta is a local before/after estimate, not a clone of any provider's tokenizer. It's consistent, which is what a savings figure needs. When the upstream returns usage, the actual counts are stored too.
  • Dollars are tokens saved Γ— price / 1,000,000, using approximate public list prices for known model families, or $1 / $3 per million for unknown ids.
  • Set GRUNT_PRICE_IN / GRUNT_PRICE_OUT to what you actually pay, otherwise the estimate won't match your invoice (and may overstate savings if your provider is cheaper than list price).
  • Output terseness is requested, not measured. projected output $ stays $0 unless you explicitly set GRUNT_ASSUME_OUTPUT_CUT, which is a labeled guess.
  • Failed upstream calls count as failures and add $0.
  • The response headers x-grunt-original-tokens, x-grunt-compressed-tokens, and x-grunt-saved-tokens expose the estimate per call. Saved tokens can even be negative when the terse instruction costs more than compression saved β€” and the proxy says so instead of hiding it.

If you set GRUNT_LOG_FILE, you get a JSONL savings ledger (prompts are not written), and grunt-proxy --report .grunt-savings.jsonl reprints a report from it without starting the server.

Safe defaults

  • Binds to 127.0.0.1 by default, so the proxy doesn't become a public credit sink.
  • No proxy-side request timeout by default, so long Claude Code or Cursor streams aren't cut off. Disconnecting the client aborts the upstream request.
  • Oversized bodies (default limit 32 MiB) are rejected with HTTP 413.

Credit where it's due

grunt-proxy is inspired by caveman by Julius Brussee β€” the idea that you can strip a lot of words from what goes to and from a model without losing meaning. grunt-proxy takes that idea and packages it as a transparent proxy with a savings report, while keeping replies in normal English.

About AtmoRouter: a cheap AI API gateway and OpenRouter alternative

Full disclosure: grunt-proxy's default upstream is AtmoRouter, but any OpenAI- or Anthropic-compatible upstream works. AtmoRouter is a cheap AI API gateway that speaks both the OpenAI Chat Completions API and the Anthropic Messages API, so one key reaches 100+ models from Cursor, Cline, Claude Code, or any compatible client. It is pay-per-token with no subscription, accepts QRIS and USDC, and prices sit well below official rates: AtmoRouter advertises an average of 83.7% below official prices, which makes it an affordable LLM API and a practical OpenRouter alternative for agent workloads.

Try it

If you run coding agents all day and want to see where your tokens go, give it a spin:

πŸ‘‰ github.com/bayuh85/grunt-proxy

Issues and PRs welcome β€” especially reports of prompts where a compression level changed the model's behavior, since that's exactly what the level knob is for.

Top comments (0)