Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions livekit-agents/livekit/agents/inference/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ def __init__(
),
)

async def _prewarm_impl(self) -> None:
await self._client.models.list()

async def aclose(self) -> None:
await super().aclose()
await self._client.close()

@classmethod
Expand Down
44 changes: 41 additions & 3 deletions livekit-agents/livekit/agents/llm/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class LLM(
def __init__(self) -> None:
super().__init__()
self._label = f"{type(self).__module__}.{type(self).__name__}"
self._prewarm_task: asyncio.Task[None] | None = None

@property
def label(self) -> str:
Expand Down Expand Up @@ -144,10 +145,47 @@ def chat(
) -> LLMStream: ...

def prewarm(self) -> None:
"""Pre-warm connection to the LLM service"""
pass
"""Pre-warm connection to the LLM service.

async def aclose(self) -> None: ...
Establishes DNS resolution and the TLS connection to the provider before the
first inference request, reducing time-to-first-token on the initial reply.
It is called automatically when an ``AgentSession`` is constructed and when an
agent activity starts, but can also be called directly.

Non-blocking (fire-and-forget), idempotent, and a no-op when no event loop
is running. Providers enable it by overriding ``_prewarm_impl``.
"""
if type(self)._prewarm_impl is LLM._prewarm_impl:
return # no provider-specific prewarm implemented

if self._prewarm_task is not None:
return

try:
asyncio.get_running_loop()
except RuntimeError:
return # no running event loop, prewarm will be retried when the agent starts

async def _prewarm() -> None:
try:
await self._prewarm_impl()
except Exception:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we clear the _prewarm_task here?

pass

self._prewarm_task = asyncio.create_task(_prewarm())

async def _prewarm_impl(self) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we add support for FallbackAdapter?

"""Provider-specific prewarm request, overriding it enables ``prewarm()``.

Implementations should perform a cheap, token-free request (e.g. listing
models) using the same client that serves chat requests, so DNS + TLS are
established and a keep-alive connection is left in the pool. Exceptions
are swallowed by ``prewarm()``; subclasses overriding ``aclose`` must call
``await super().aclose()`` to cancel an in-flight prewarm."""

async def aclose(self) -> None:
if self._prewarm_task is not None:
await aio.cancel_and_wait(self._prewarm_task)

async def __aenter__(self) -> LLM:
return self
Expand Down
11 changes: 10 additions & 1 deletion livekit-agents/livekit/agents/voice/agent_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from .. import cli, inference, llm, stt, tts, utils, vad
from .._exceptions import APIError
from ..job import get_job_context
from ..llm import AgentHandoff, ChatContext, MetricsReport
from ..llm import LLM, AgentHandoff, ChatContext, MetricsReport
from ..llm.chat_context import Instructions
from ..log import logger
from ..metrics import AgentSessionUsage, ModelUsageCollector
Expand Down Expand Up @@ -477,6 +477,15 @@ def __init__(
self._llm = llm or None
self._tts = tts or None

# eagerly establish DNS/TLS to the LLM provider so the first inference
# request doesn't pay connection setup costs
if isinstance(self._llm, LLM):
try:
asyncio.get_running_loop()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is always an event loop

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding to this, I think we can skip checking it here since _llm.prewarm already does that internally.

self._llm.prewarm()
except RuntimeError:
pass # no running event loop, prewarm runs at agent activity start instead

self._keyterm_detector = KeytermDetector(
static_keyterms=self._opts.stt_context_options["keyterms"],
options=self._opts.stt_context_options["keyterm_detection"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ def __init__(
),
)

async def _prewarm_impl(self) -> None:
await self._client.models.list(limit=1)

@property
def model(self) -> str:
return self._opts.model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ def __init__(
# Store thought_signatures for Gemini 2.5+ multi-turn function calling
self._thought_signatures: dict[str, bytes] = {}

async def _prewarm_impl(self) -> None:
# also fetches auth tokens ahead of time on vertexai
await self._client.aio.models.list(config={"page_size": 1})

@property
def model(self) -> str:
return self._opts.model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def __init__(
raise ValueError("Mistral AI API key is required. Set MISTRAL_API_KEY or pass api_key")
self._client = client or Mistral(api_key=mistral_api_key)

async def _prewarm_impl(self) -> None:
await self._client.models.list_async()

@property
def model(self) -> str:
return self._opts.model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,13 @@ def __init__(
),
)

async def _prewarm_impl(self) -> None:
# token-free request supported by openai and openai-compatible servers
await self._client.models.list()

async def aclose(self) -> None:
await super().aclose()

if self._owns_client:
await self._client.close()

Expand Down
48 changes: 15 additions & 33 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading