IQ Routing is a drop-in gateway. We classify each request, serve from cache, and route to the cheapest model that clears the quality bar and completes the task. The insight others miss: an agent is a trajectory, not a stream of independent calls. We route each step based on where it sits in the run: a cheap model on boilerplate, the strongest model on what matters.
Hi Product Hunt. I'm George, and I built IQ Routing solo.
The idea started from a problem I kept seeing in my own AI-agent usage: agent runs can get expensive fast, and a lot of that spend is hard to justify.
Most routing tools treat an agent run like a pile of separate requests. They look at each request on its own and try to make that single request cheaper or faster. But that misses how agents actually work.
An agent run is one flow from start to finish. Some steps are busywork. Some steps are context-building. A few steps are the decisions that determine whether the final result is good or bad. Paying for the strongest model on every step is wasteful. Using a weaker model everywhere is risky. The right answer is to spend more only where the work actually needs it.
That is what IQ Routing does. It sits between your agent tools and model providers, looks at each step in the run, and picks the cheapest model that can still do that step well. It also reuses work it has seen before, so you are not paying repeatedly for answers the system already knows.
The setup is meant to be boring in a good way: point your existing tool at IQ, keep your code the same, and keep working. IQ works today with Claude Code, Codex, and other popular apps
This is the launch. Try it, break it, and tell me where the routing feels wrong. I read every reply.
Report
Does the cache reuse cross runs? That is the piece I can't place under your own trajectory framing. If it does, a step gets handed an answer produced under a different set of earlier steps, so a request that reads identically can sit at a very different point in the run.
Haven't tried it yet so this may well be handled. It's the first thing I'd look for in the docs.
Report
Maker
@dimhold The cache key for any request with prior conversation turns is built from the full history up to that point, not just the current message. A cache hit only occurs when the entire trajectory leading into that step matches as well, so a change anywhere earlier in the run changes the key and produces a miss instead of a hit. That’s the mechanism that rules out a step being served an answer computed under a different set of earlier steps.
The one exception is the first call of a run, before any history exists to hash. That request is matched on content alone. Since there’s no prior-step context at that point, it falls outside the scenario you’re describing.
Report
@george_avila That closes it, thanks. Full-history keying was the part I could not see from outside.
The first call is the one I would still watch. My own runs open with a byte-identical first message every time, same system prompt and same task template, while the repo underneath has moved. A content-only match hits there. What comes back was computed against a different working tree.
I have not run IQ, so I may be reading that exception too broadly. Do you scope the first-call key by anything outside the message?
Report
Maker
@dimhold A cached response is matched only on the content of the request itself, meaning the instructions and the message text, rather than on anything about your environment or repository state. Given that, an identical opening message can return a cached response even after what’s actually on disk has changed since that response was generated, and the same holds for near-duplicate opening messages, not just exact ones.
To limit that exposure, cached responses expire on a schedule rather than staying valid indefinitely, and responses involving tool use are given a much shorter shelf life than plain text responses, since they typically depend more on something outside the request itself. That narrows the exposure window, but it does not close it entirely.
If a live call is ever required for a specific request, sending a request header (x-iqr-cache: off) opts that call out of caching entirely, and nothing else about it changes.
Report
@george_avila That is the answer I was after, thanks. The shorter shelf life for responses that involve tool use is the part I would not have guessed.
For my case the header is the simple fix: set x-iqr-cache: off on the first call of a run and leave everything after it cached. The opening message is the one place where identical text does not mean identical world.
Does the cache reuse cross runs? That is the piece I can't place under your own trajectory framing. If it does, a step gets handed an answer produced under a different set of earlier steps, so a request that reads identically can sit at a very different point in the run.
Haven't tried it yet so this may well be handled. It's the first thing I'd look for in the docs.
@dimhold The cache key for any request with prior conversation turns is built from the full history up to that point, not just the current message. A cache hit only occurs when the entire trajectory leading into that step matches as well, so a change anywhere earlier in the run changes the key and produces a miss instead of a hit. That’s the mechanism that rules out a step being served an answer computed under a different set of earlier steps.
The one exception is the first call of a run, before any history exists to hash. That request is matched on content alone. Since there’s no prior-step context at that point, it falls outside the scenario you’re describing.
@george_avila That closes it, thanks. Full-history keying was the part I could not see from outside.
The first call is the one I would still watch. My own runs open with a byte-identical first message every time, same system prompt and same task template, while the repo underneath has moved. A content-only match hits there. What comes back was computed against a different working tree.
I have not run IQ, so I may be reading that exception too broadly. Do you scope the first-call key by anything outside the message?
@dimhold A cached response is matched only on the content of the request itself, meaning the instructions and the message text, rather than on anything about your environment or repository state. Given that, an identical opening message can return a cached response even after what’s actually on disk has changed since that response was generated, and the same holds for near-duplicate opening messages, not just exact ones.
To limit that exposure, cached responses expire on a schedule rather than staying valid indefinitely, and responses involving tool use are given a much shorter shelf life than plain text responses, since they typically depend more on something outside the request itself. That narrows the exposure window, but it does not close it entirely.
If a live call is ever required for a specific request, sending a request header (x-iqr-cache: off) opts that call out of caching entirely, and nothing else about it changes.
@george_avila That is the answer I was after, thanks. The shorter shelf life for responses that involve tool use is the part I would not have guessed.
For my case the header is the simple fix: set x-iqr-cache: off on the first call of a run and leave everything after it cached. The opening message is the one place where identical text does not mean identical world.