feat(simulation): pin text simulations to the low inference class - #6721
Open
theomonnom wants to merge 7 commits into
Open
feat(simulation): pin text simulations to the low inference class#6721theomonnom wants to merge 7 commits into
theomonnom wants to merge 7 commits into
Conversation
A text simulation is batch load: a run fans out many jobs at once and nobody is waiting on the answers. Those requests should not compete with live traffic for gateway capacity, so pin every LiveKit Inference request made from a text simulation to inference class "low". Set in get_inference_headers(), which is the single chokepoint every LiveKit Inference model builds its headers through (LLM, STT, TTS, avatar, interruption detection, end-of-turn transports), so all of them are covered at once. It is applied last and inference.LLM now yields to it, so an explicitly configured inference_class cannot claim priority capacity from inside a simulation. Audio simulations are deliberately excluded: they run in real time against the audio pipeline, so their latency has to stay representative of production. Also folds the job-context lookup behind simulation.current_simulation(), reused by AgentSession._text_only.
- current_simulation() now lives next to get_job_context in job.py, which is where the job-context accessors belong. - get_inference_headers() takes the caller's configured inference_class and returns the resolved header, so one function owns the precedence. inference.LLM hands its class over and no longer sets the priority header itself. - the existing text-simulation log line in AgentSession.start now names the inference class too, so the demotion is visible at session start.
get_inference_headers() had no business knowing what a simulation is. The inference layer now just exposes a knob, pin_inference_class(), and the job runner decides what deserves pinning: job._pin_simulation_inference_class runs right after the job context is set, before the entrypoint, so every model the job goes on to build inherits the class. This drops the ..job / ..simulation imports from inference/_utils, moves the "text simulations are batch load" rationale next to the code that acts on it, and makes the pin greppable instead of implicit. The pin is a contextvar set inside the job's own task, so it cannot leak across jobs sharing a process. Reverts the test-file churn, no longer needed: the header builder does not consult the simulation context anymore.
Replaces the contextvar pin with JobContext.inference_headers: the extra headers a job puts on every LiveKit Inference request it makes. A text simulation contributes the low class there; an ordinary job contributes nothing. get_inference_headers() merges it last, so what the job asserts about itself outranks what an individual model was configured with, and the inference layer never has to name a simulation. It already reads the job context for the room/job/agent headers, so this adds no new dependency, just one more thing to ask the job for. No job-runner hook and no process state: the property is read at request time off the job context that is already there.
davidzhao
approved these changes
Aug 6, 2026
They asserted dict keys against hand-rolled JobContext duck types, so every time the header builder asks the job for one more thing the fakes have to grow another stub. The guards they covered (the isconnected() check before touching local_participant, from #5947, and the isinstance sid checks) are untouched in the source.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Text simulations run as batch load: a run fans out many jobs at once and nobody is waiting on the answers. Those requests currently reach the inference gateway at the same class as live user traffic. This pins every LiveKit Inference request made from a text simulation to inference class
low.How
JobContext.inference_headersis the extra headers a job puts on every LiveKit Inference request it makes. A text simulation contributes the low class; an ordinary job contributes nothing.get_inference_headers()merges it last, so what the job asserts about itself outranks what an individual model was configured with:prioritylowprioritypriorityprioritypriorityThe inference layer never names a simulation. It already reads the job context for the room/job/agent headers, so this adds no new dependency, just one more thing to ask the job for. There is no job-runner hook and no process state: the property is read at request time off the job context that is already in scope.
Because it lands in the shared header builder, every LiveKit Inference model is covered: LLM, STT/TTS, avatar, interruption detection, and the end-of-turn transports. That includes models an
AgentSessionnever sees, which is why it is resolved per request rather than set on the session's models at start: per-agent LLMs from a handoff, anAgentTask's own LLM, and anyllm.chat()a tool calls directly would each need their own injection otherwise.Audio simulations are deliberately excluded. They run in real time against the audio pipeline, so their latency has to stay representative of production. The gate is
SIMULATION_MODE_TEXTonly.current_simulation()is added next toget_job_contextinjob.py: the job-context-to-simulation lookupAgentSession._text_onlywas doing inline.Note on the gateway side
The gateway today recognizes only
standardandpriorityonX-LiveKit-Inference-Priorityand maps anything else tostandard(ComputeInferenceTiersWithServed). Solowis currently equivalent tostandard, i.e. this is a no-op on billing and scheduling until a low tier lands gateway-side. It is deliberately sent now so the agents side is already correct when that happens, and so simulations stop being able to requestpriority.lowis intentionally kept out of the publicInferenceClassliteral until the gateway honors it.Testing
make type-checkclean (629 source files),make lintand format check cleanuv run pytest --unit: 1415 passed (the 9 teardown errors are pre-existing OTel exporter noise, present onmainwithout this change)inference.LLM: a realSimulationDispatchproto on a realagent.Jobinto a realJobContext, throughget_inference_headers()and the openai client, to a local HTTP server recording what actually arrived on the wire. All rows of the table above match, plusSIMULATION_MODE_UNSPECIFIED(treated as text, so also demoted) and the no-argument call site STT/TTS/avatar/eot use. The room and job identification headers still flow unchanged.tests/test_inference_utils.pyis removed. It asserted dict keys against hand-rolledJobContextduck types, so every time the header builder asks the job for one more thing the fakes have to grow another stub. The guards it covered are untouched in the source: theisconnected()check before touchinglocal_participant(from #5947) and theisinstancesid checks. Restore withgit checkout origin/main -- tests/test_inference_utils.pyif you'd rather keep it.