Back to blog

AI patterns

LLM Routers Compared: What Happens Between Your Application and the Model

Routers promise many models behind one API, failover and processing in the EU. What they do with caching, reasoning effort and prices is rarely in the brochure. We measured Cortecs, EU Router, Requesty and OpenRouter with the same calls as Anthropic direct.

SvK by Sven von Känel 11 min read
  • KI
  • LLM-Router
  • Prompt Caching
  • EU

TLDR

Through Cortecs, EU Router, Requesty and OpenRouter, Claude caches only if you send cache_control — but then it hits in 4 of 4 follow-up calls at all four. Without the marker: 0 of 4. The router decides which upstream answers, and with it whether caching happens and what it costs. Routers accept reasoning efforts that the provider rejects. All four report the cost per call, and for Claude it matched to the cent; for open-weight models, the charged price ranged from 80 % below to 170 % above the listed one. Part 4 of the series "What an LLM call really costs".

Introduction

A router is an API in front of many APIs. You send a request in the OpenAI format, and the router forwards it to a suitable provider — Anthropic, AWS Bedrock, Google Vertex, an open-weight host. For European companies, there's a third argument: routers such as Cortecs or EU Router promise that processing stays in the EU.

It's convenient in any case. Whether it's also cheap depends on details you only see when you measure.

Disclosure: evanto uses Cortecs as its partner for Managed AI in CompanyWizard. The measurements ran on separate measurement accounts; Cortecs didn't see the results before publication.

1. The test setup

We sent the same Claude Sonnet 5 call with a system prompt of about 7,200 tokens five times, 20 seconds apart — directly to Anthropic and through four routers. Each in two variants:

  • Best effort: with cache_control (TTL 1 hour) and, where possible, a fixed upstream.
  • Unchanged: without a marker, the way most applications call Claude through a router.

At OpenRouter, we added a third variant: cache_control with a session key, but without a fixed upstream.

2. Caching through the router

The path of a Claude call through a router

Path Hits in calls 2–5, best effort Hits, unchanged Upstream
Anthropic direct 4/4 0/4 –
Cortecs (residency) 4/4 (pinned to Bedrock) 0/4 amazon_ireland or google
EU Router 4/4 0/4 aws-bedrock
Requesty 4/4 0/4 not reported
Requesty with auto_cache only 4/4 – not reported
OpenRouter, provider.order = Anthropic 4/4 0/4 Anthropic or Claude Platform on AWS
OpenRouter, session key only 4/4 – Claude Platform on AWS

The pattern is clear: without cache_control, Claude doesn't cache through any router; with the marker, it caches through all of them.

In detail:

  • EU Router passes cache_control through, although its documentation doesn't mention it.
  • Requesty sets the breakpoints itself with "requesty": {"auto_cache": true} — without any change to the prompt. Requesty reports cache writes as caching_tokens, split into 5 minutes and 1 hour.
  • OpenRouter passes cache_control through, including the 1-hour TTL , and kept all follow-up calls with a session key on the same upstream — sticky routing works.
  • Cortecs caches only when the upstream caches. Without a preference, every one of our calls went to Google Vertex — without a single hit. Pinned to amazon_ireland: 4 of 4. A provider switch between calls wasn't the problem; the router consistently stayed with the upstream that doesn't cache.

Lesson Learned: With a router, caching isn't a property of the model but of the path. If you want the saving, you have to fix the path.

3. Who actually answers?

Cortecs and EU Router name the chosen upstream in a response header (x-cortecs-provider, x-provider-slug), OpenRouter in the provider field of the response, and Requesty not at all. That's more than a detail: at Cortecs, the same prompt counted 5,426 prompt tokens on amazon_ireland and 7,238 on google. So the upstream affects not only the cache, but also the number you're billed for.

Tip: Log the upstream header for every call. Without it, you can't explain cost jumps after the fact.

4. Reasoning effort through the router

Routers tend to accept everything. Cortecs accepted all seven effort values for Claude, including none and minimal, which Anthropic direct rejects with HTTP 400. EU Router also accepted every value for Claude. Requesty reports no reasoning tokens for Claude, so you can't check whether the value arrives. OpenRouter also accepted all seven values for Claude, but rejected none for gpt-oss-120b.

Cortecs documents that it translates effort for Claude into thinking budgets of 1,024 to 4,096 tokens (not measurable). Requesty names budgets of up to 16,384 tokens (not measurable). Both are translations that newer Claude models no longer accept, according to Anthropic — a direct budget_tokens call ended with HTTP 400 in our test. What the router actually passes on remains a black box.

5. What it costs

All four routers report costs in every response:

  • Requesty in USD (usage.cost).
  • OpenRouter also in USD (usage.cost).
  • EU Router in USD and euros, converted at the daily ECB rate.
  • Cortecs as an integer in micro-euros — contrary to its own API reference, which lists no cost in the response. Cortecs also returns the cache fields, unlike what the API reference suggests.

For Claude, these costs matched our formula to the cent. Not so for open-weight models:

Router Model Charged vs. listed (median)
EU Router gpt-oss-120b +103 %
Cortecs gpt-oss-120b +50 % to +67 %
Cortecs DeepSeek V4.1 Flash +44 %
Requesty gpt-oss-120b via Fireworks 0 %
OpenRouter gpt-oss-120b −75 %

The listed price often doesn't match the upstream that actually answers — at Cortecs and EU Router it was more expensive, at OpenRouter clearly cheaper. Requesty avoids this because you fix the upstream in the model name (fireworks/gpt-oss-120b).

And there are surcharges too: EU Router lists Claude Sonnet 5 at $2.20 instead of $2.00 per million input tokens — 10 % above Anthropic, OpenRouter and Requesty.

6. Open-weight models: the host decides

For open-weight models, the router is only half the story; what matters is the host behind it. The same gpt-oss-120b cached fully at Cortecs and at Requesty via Fireworks, and not at all at EU Router and IONOS. The accepted effort values also differ by host — IONOS even rejected high for Qwen3.8-27B, but not xhigh.

7. What "EU" means at each router

"EU" isn't a uniform promise at routers:

  • Cortecs separates accounts or keys: with the sovereign key, 49 models were available, without Claude; with the residency key, 87, including Claude via Bedrock and Vertex.
  • EU Router filters providers by data_residency, eu_owned and max_retention_days (not measurable).
  • Requesty's EU endpoint keeps only Requesty's own processing in the EU; whether inference also runs there depends on the model you choose (not measurable).

Whether data really never leaves the EU can't be measured from the outside. What you can measure is which upstream answers — see section 3.

8. How we solved it in .NET

In Evanto.ChatClients, all routers run over the OpenAI-compatible Chat Completions API, even when they're configured as "OpenAI". The Responses API is used only for the real OpenAI host:

internal static Boolean UsesResponsesApi(EvChatClientSettings settings)
    => String.Equals(settings.ProviderName, "OpenAI", StringComparison.OrdinalIgnoreCase)
       && Uri.TryCreate(settings.Endpoint, UriKind.Absolute, out var endpoint)
       && String.Equals(endpoint.Host, OPENAI_API_HOST, StringComparison.OrdinalIgnoreCase);

Source: UsesResponsesApi (excerpt)

CompanyWizard sets router-specific fields such as eu_native (Cortecs) or rule_name (EU Router) through freely configurable extra fields in the request body.

What's still missing on our side: so far, we allow cache_control only for the Anthropic provider type. So a Claude model behind a router gets no explicit caching — exactly the variant that brought 0 of 4 hits in this measurement. The gap is known; it isn't closed yet.

9. What we deliberately left open

  • Load and failures: we didn't test failover behaviour under load.
  • Data flows: where data is physically processed can't be checked from the outside.

Conclusion: patterns you can reuse

  1. Always send cache_control when Claude runs through a router — or turn on auto_cache at Requesty.
  2. Fix the upstream if cache or price matters.
  3. Log the upstream header so you can explain costs after the fact.
  4. Check effort values against the provider, not the router — the router accepts almost everything.
  5. For open-weight models, choose the host, not just the model.

All raw data and scripts: github.com/evanto-media-ag/llm-caching-reasoning-costs

The series

References


Want to run Claude or open-weight models through an EU router? Let's spend 30 minutes on your specific workflow — no obligation, no cost, with concrete next steps at the end. Book a 30-minute call

Note: Article created with AI assistance.

NEWSLETTER

Four to six times a year, no marketing noise.

One pattern, one case, one recommendation. Signup with double opt-in, unsubscribe at any time.