Two OpenRouter Cost Traps I Found by Reading Receipts, Not Docs

Reasoning effort defaulting to max cost 6x on a task that needed no thinking, and two identical calls billed 2.2x apart because the provider kept changing. Both are now hard-coded against in the wrapper that runs my loop's utility coding.

I run a small autonomous loop that builds and operates SaaS products, and its budget for utility model calls is $20 a month. That number is not a rounding error to me, it is the constraint everything else is designed around. Most of the loop's mechanical work (refactors, test scaffolds, regexes, JSON munging, bulk classification) goes through OpenRouter to cheap open models, and I wrapped those calls in a single script so the defaults live in one place.

When I sat down to measure what the wrapper was actually spending, two cost traps showed up. Neither is a bug in OpenRouter. Both are defaults that are reasonable for a person clicking around a playground and expensive for a program making thousands of calls. The OpenRouter help center even documents the general shape of both ("Why was I charged more per token than the price shown on the model page?" is literally an article title). What I could not find anywhere was the arithmetic: what each trap actually cost on a real task, and what fixing it was worth. So here are my numbers.

Trap 1: reasoning effort defaults to max

The default code tier in my wrapper uses GLM 5.3 Flash, a cheap model with strong coding benchmarks. On 2026-09-09 I benchmarked it against the live model list and one real coding task, and the first receipt stopped me.

The task was a small function. The answer was 99 tokens. The model spent 582 reasoning tokens to produce it, because reasoning effort defaulted to "max". Same task, same correct output, at effort "low": $0.00006. At the default: $0.00036. Six times the cost, identical result, and the only difference was a field I had not set.

This is easy to miss because the per-token prices on the model page are correct. Reasoning tokens are billed as output tokens, so the model page's output price is exactly right. What is missing is the "how many tokens will you actually get" part, and for this model the default effort produces hundreds of thinking tokens for answers that need none.

The rule in the wrapper is now absolute: reasoning effort is always set explicitly. There is no code path that sends a request without a reasoning field. The tiers look like this:

TierModelReasoningWhat it is for
bulkDeepSeek v4 Flashoffcheapest correct output for mechanical work
code (default)GLM 5.3 Flasheffort: lowbest coding-per-dollar; the everyday tier
reasonGLM 5.3 Flasheffort: highthe same model when a task is genuinely tricky
hardGLM 5.3 fulleffort: lowescalation, batch-only, half price, 24h turnaround

The interesting design decision is the "reason" tier: it is the same model as "code" with effort high, not a bigger model. Moving up a tier costs about 6x the tokens, so the tier exists to be rare, and the wrapper refuses to route the "hard" tier through a synchronous call at all (batch only, half price).

One more surprise in the same area: the batch API's 50% discount is not universal. glm-5.3-flash:batch is priced identically to glm-5.3-flash sync. The discount is real above the flash tier and meaningless at the bottom. If your cost model assumes "batch = half price", you will be wrong in the cheap tier, which is exactly where high-volume work lives.

Trap 2: identical calls, 2.2x apart, and a cache that never hits

The second trap showed up as variance. The same model, same tokens, billed $0.00183 on one call and $0.00083 on another. A model on OpenRouter is served by multiple providers at different prices, and the default load balancer moves between them. It is weighted toward cheaper endpoints, but it is not a hard rule, and with tool calls the default ordering ignores price entirely.

The 2.2x spread on its own is annoying but survivable. The worse part is what the churn does to the prompt cache. My wrapper sends a stable prefix on every call: a system block, then any attached files, then the varying instruction last. The whole point is that the provider caches the prefix and the repeated calls get cheap. But the cache lives at the provider. Route to provider A, then provider B, then A again, and you never hit anyone's cache.

Setting provider: { sort: "price" } on every request fixed both problems at once. The calls stopped moving between providers, which stopped the 2.2x spread, and the price sort is stable enough that consecutive calls land on the same endpoint, which is where the cache comes back. The measured effect: a call that cost $0.00224 with an empty cache dropped to $0.00085 with a 11,520-token cache hit. On a wrapper that makes the same shape of call all day, the cache hit is most of the savings; the price sort is just what makes the hits possible.

The general rule: whatever makes your calls identical is worth more than it looks, because identical calls are cacheable calls, and the cache discount is bigger than any per-token price difference you can negotiate by model choice.

The wrapper's other accounting rules

Measuring the two traps required an accounting layer, and writing it surfaced three smaller lessons worth passing on.

Batch results do not carry a per-result cost. I probed this against the live API on 2026-09-10. A batched result's usage carries token counts but no cost field, and usage: { include: true } in the request does not add one. The charge is reported once, on the batch object itself. My first version summed per-result costs, got 0 every time, and batch spend was invisible to the budget cap. If you are building a batch pipeline, sum nothing: read the batch total.

Book the difference, not the event. A batch gets fetched more than once (poll, then read, then re-read). Billing the full cost on every fetch double-books it. Billing on a "seen it" flag loses a later, larger authoritative number. Booking the difference between what the provider says the batch cost and what the ledger has already booked for it handles both: repeat fetches are free, and a corrected total still lands.

The budget stop should gate only new spending. The wrapper refuses to start anything at 90% of the $20 cap. But batch status and batch fetch cost nothing, and they are how an already-paid-for batch gets read and booked. Blocking them at the cap would strand paid results and prevent the ledger from recording the very charge that pushed it over. The stop has to know the difference between spending and reading.

What I would tell someone starting this

  • Set reasoning effort explicitly on every request. If a model accepts it, not setting it is a decision you did not make.
  • Set provider.sort: "price" (or pin the provider) on every request, including batch bodies. Omitting it on the batch path is how my batch calls skipped the sort.
  • Keep the stable prefix first in the message list, and never interleave the varying part before it.
  • Log cost per call to an append-only file from day one. Both traps were invisible until I could aggregate receipts.
  • Verify your batch discount by reading one actual batch's price. Mine was 0% in the flash tier.
  • Treat every default in the routing config as a line item. "It has a sensible default" and "it costs 6x less when I override it" are both true.

The wrapper is still a draft by my own loop's rules: nothing a model writes ships unverified, and that includes the wrapper itself. But the two hard-coded defaults have been in every request since the measurement, and the spend ledger says the difference. Six times and 2.2x are the two numbers I would want anyone routing high-volume calls through a model gateway to check on their own receipts before trusting a single headline price.