Skip to content
Merged
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
30 changes: 19 additions & 11 deletions src/openrtc/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ class _ProviderRef:
kwargs: dict[str, Any]


# ``(module, qualname)`` pairs for plugin classes that expose OpenAI-style
# ``_opts`` and can be rehydrated via ``ProviderClass(**kwargs)`` in the child
# after unpickling. Add entries here when new LiveKit plugins follow the same
# pattern (Deepgram, Azure, …); unknown classes fall back to pickle or error.
# ``(module, qualname)`` pairs for plugin classes known to expose ``_opts``
# and rehydrate via ``ProviderClass(**kwargs)``. The generic path in
# ``_try_build_provider_ref`` now handles any ``livekit.plugins.*`` class with
# ``_opts``, so this set is a fast-path / documentation of tested providers.
_PROVIDER_REF_KEYS: frozenset[tuple[str, str]] = frozenset(
{
("livekit.plugins.openai.stt", "STT"),
Expand Down Expand Up @@ -602,13 +602,21 @@ def _deserialize_provider_value(value: Any) -> Any:
def _try_build_provider_ref(value: Any) -> _ProviderRef | None:
cls = type(value)
key = (cls.__module__, cls.__qualname__)
if key not in _PROVIDER_REF_KEYS:
return None
return _ProviderRef(
module_name=key[0],
qualname=key[1],
kwargs=_extract_provider_kwargs(value),
)
# Fast path: known providers
if key in _PROVIDER_REF_KEYS:
return _ProviderRef(
module_name=key[0],
qualname=key[1],
kwargs=_extract_provider_kwargs(value),
)
# Generic path: any livekit plugin with _opts
if cls.__module__.startswith("livekit.plugins.") and hasattr(value, "_opts"):
return _ProviderRef(
module_name=cls.__module__,
qualname=cls.__qualname__,
kwargs=_extract_provider_kwargs(value),
)
Comment on lines +612 to +618
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The generic provider-ref path triggers purely on cls.__module__.startswith('livekit.plugins.') + hasattr(value, '_opts'), without verifying that the referenced module/class is actually importable/resolvable. This can make a config appear spawn-safe (because _ProviderRef is pickleable) but then fail during unpickling when _deserialize_provider_value calls importlib.import_module(...) / _resolve_qualname(...). Consider guarding the generic path by confirming the module can be imported (or at least importlib.util.find_spec is not None) and that _opts is non-None, otherwise fall back to the existing pickleability check so failures happen at registration time rather than in the worker.

Copilot uses AI. Check for mistakes.
return None


def _extract_provider_kwargs(value: Any) -> dict[str, Any]:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,23 @@ def test_agent_config_is_pickleable_with_openai_provider_objects(
assert restored.tts.__class__.__module__ == "livekit.plugins.openai.tts"


def test_generic_livekit_plugin_with_opts_is_accepted() -> None:
"""Non-OpenAI livekit plugins with ``_opts`` should serialize via the generic path."""

class FakeOpts:
model = "nova-3"

# Create a class whose module looks like a livekit plugin
FakeSTT = type("STT", (), {"__module__": "livekit.plugins.deepgram.stt"})
instance = FakeSTT()
instance._opts = FakeOpts() # type: ignore[attr-defined]

pool = AgentPool()
# Should not raise — the generic _opts path handles it
config = pool.add("test", DemoAgent, stt=instance)
assert config.stt is not None
Comment on lines +272 to +286
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This test only asserts that pool.add(...) does not raise, but it doesn't validate the intended behavior (pickle/rehydration via the generic _opts path). As written, it also sets model as a class attribute on FakeOpts, so _extract_provider_kwargs (which uses vars(options)) will extract {} and drop model, making the test pass even if option serialization is broken. To properly cover the new behavior, ensure the fake plugin module/class is importable (e.g., via a temporary module in sys.modules) and assert a pickle round-trip restores the provider type and preserves extracted kwargs like model.

Copilot uses AI. Check for mistakes.


def test_list_agents_returns_registration_order() -> None:
pool = AgentPool()
pool.add("restaurant", DemoAgent)
Expand Down
Loading