run_id and the run is scheduled as a single unit, pinned to the worker holding its KV cache. Each turn prefills only what changed since the last one, at the 90%-off cached-input rate.
Untagged, every turn looks like an unrelated request. It lands on any free worker and re-prefills the whole conversation at full price.
Tagged runs get:
- Sticky placement. Every turn routes to the worker that already holds the run’s cache.
- Priority resume. A run coming back from a tool call is admitted ahead of new arrivals.
- Whole-run admission. Under load, whole runs pause instead of every run getting slow.
Available on Kimi K3 (
morph-kimik3, morph-kimik3-fast). Requests without a run id are unaffected: they route exactly as they do today, on the same workers.Quick Start
- cURL
- Python
- TypeScript
A multi-turn loop
Send the samerun_id on every turn of one run, then release it when the run ends:
Reference
Three top-level body fields, all sent the way any non-standard field is (extra_body in the Python SDK):
program_id, parent_program_id, and program_final are accepted as drop-in aliases, so clients written to the ThunderAgent convention (SkyRL, OpenHands) work unchanged.
Ids must be printable ASCII, and are capped at 256 characters. An id that is empty, whitespace-only, or carries a control or non-ASCII character counts as absent: the request is served untagged rather than rejected, because a bad scheduler hint should cost you the optimization and never the request. A run_final request with no run_id has nothing to release and is served as an ordinary request.
Subagents
A fan-out gets one run per subagent, each naming the run that spawned it. Every branch is scheduled on its own, with its own sticky worker, so a subagent that takes several turns keeps its cache across them the same way a root run does. Give each branch a distinct id: reusing the parent’s id across concurrent branches makes the scheduler treat them as one unit and pin them all to one worker.parent_run_id records lineage. It is what ties a fan-out together in traces and attribution; it does not currently pull a child toward its parent’s worker.
Best practices
- One id per run, generated at the start. A UUID or your own trace id. Reuse it for every turn including retries of the same turn.
- Never share an id across unrelated runs. The scheduler will pin them to one worker and account for them together, so they fight over the same cache.
- Always send the release. One fire-and-forget call in whatever cleanup path your agent already has, ideally the
finallyrather than the happy path. The 15-minute reclaim is a backstop for crashes, not a substitute: until it fires, the run is still holding a slot. - Keep the prefix stable too. Stickiness gets your turns to the worker holding the cache; prefix caching is what makes those tokens cheap. On Kimi K3 cached input is 90% off, 2.90. A multi-turn loop with a stable prefix and a run id is the case both are built for.
- Tag from the agent, not the gateway. The id has to follow one logical run. A per-process or per-user id collapses concurrent runs into one.
Pitfalls
cached_tokens didn't improve after tagging
cached_tokens didn't improve after tagging

prompt_tokens_details.cached_tokens on an untagged run first: if it was already near zero, the problem is prefix stability, not placement. See Prompt Caching.Latency got worse under load
Latency got worse under load

The release call returned an empty response
The release call returned an empty response

run_final short-circuits before the model, so there are no choices and no completion tokens. Don’t parse it for content.See Also
- Prompt Caching — the 90%-off cached-input rate stickiness is protecting
- Open Source Models — Kimi K3 pricing and the model list
- Compact — shrink an agent’s context before it grows past what caching saves