Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -860,9 +860,9 @@ def with_telnyx(
@staticmethod
def with_nebius(
*,
model: str | NebiusChatModels = "meta-llama/Meta-Llama-3.1-70B-Instruct",
model: str | NebiusChatModels,
api_key: str | None = None,
base_url: str = "https://api.studio.nebius.com/v1/",
base_url: str = "https://api.tokenfactory.nebius.com/v1/",
client: openai.AsyncClient | None = None,
user: NotGivenOr[str] = NOT_GIVEN,
temperature: NotGivenOr[float] = NOT_GIVEN,
Expand All @@ -874,10 +874,11 @@ def with_nebius(
top_p: NotGivenOr[float] = NOT_GIVEN,
) -> LLM:
"""
Create a new instance of Nebius LLM.
Create a new Nebius Token Factory LLM.

``api_key`` must be set to your Nebius API key, either using the argument or by setting
the ``NEBIUS_API_KEY`` environmental variable.
``model`` must be a current chat-completions model from the Token Factory model catalog.
``api_key`` must be set to your Token Factory API key, either using the argument or by
setting the ``NEBIUS_API_KEY`` environmental variable.
"""

api_key = api_key or os.environ.get("NEBIUS_API_KEY")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,33 +80,19 @@
]

NebiusChatModels = Literal[
"meta-llama/Meta-Llama-3.1-70B-Instruct",
"meta-llama/Llama-3.3-70B-Instruct",
"meta-llama/Llama-3.3-8B-Instruct",
"meta-llama/Meta-Llama-3.1-405B-Instruct",
"openai/gpt-oss-120b",
"openai/gpt-oss-20b",
"moonshotai/Kimi-K2-Instruct",
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
"NousResearch/Hermes-4-405B",
"NousResearch/Hermes-4-70B",
"zai-org/GLM-4.5",
"zai-org/GLM-4.5-Air",
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-R1",
"deepseek-ai/DeepSeek-V3",
"deepseek-ai/DeepSeek-V3-0324",
"Qwen/Qwen3-235B-A22B-Instruct-2507",
"Qwen/Qwen3-235B-A22B",
"Qwen/Qwen3-32B",
"Qwen/Qwen3-30B-A3B",
"Qwen/Qwen3-4B-fast",
"Qwen/Qwen3-14B",
"Qwen/Qwen2.5-Coder-7B",
"Qwen/Qwen2.5-Coder-32B-Instruct",
"nvidia/Llama-3_1-Nemotron-Ultra-253B-v1",
"mistralai/Mistral-Nemo-Instruct-2407",
"google/gemma-2-2b-it",
"moonshotai/Kimi-K3",
"moonshotai/Kimi-K2.7-Code",
"moonshotai/Kimi-K2.6",
"zai-org/GLM-5.2",
"zai-org/GLM-5.1",
"deepseek-ai/DeepSeek-V4-Pro",
"deepseek-ai/DeepSeek-V4-Flash",
"Qwen/Qwen3.5-397B-A17B",
"nvidia/Nemotron-3_5-Lightning",
"nvidia/Nemotron-3-Ultra-550b-a55b",
"MiniMaxAI/MiniMax-M3",
]

CerebrasChatModels = Literal[
Expand Down
84 changes: 84 additions & 0 deletions tests/test_plugin_openai_nebius.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

import json
from unittest.mock import patch

import httpx
import openai
import pytest

from livekit.agents.llm import ChatContext
from livekit.plugins.openai import LLM

pytestmark = pytest.mark.unit

_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
_BASE_URL = "https://api.tokenfactory.nebius.com/v1/"
_STREAM_RESPONSE = b"""data: {"id":"chatcmpl-test","choices":[{"delta":{"content":"ok","role":"assistant"},"finish_reason":null,"index":0}],"created":0,"model":"meta-llama/Llama-3.3-70B-Instruct","object":"chat.completion.chunk"}

data: {"id":"chatcmpl-test","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":0,"model":"meta-llama/Llama-3.3-70B-Instruct","object":"chat.completion.chunk"}

data: [DONE]

"""


class _RecordingTransport(httpx.AsyncBaseTransport):
def __init__(self) -> None:
self.requests: list[httpx.Request] = []

async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
await request.aread()
self.requests.append(request)
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
content=_STREAM_RESPONSE,
request=request,
)


def test_nebius_helper_configures_token_factory(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("NEBIUS_API_KEY", "env-key")

with patch("livekit.plugins.openai.llm.openai.AsyncClient") as client_cls:
client = client_cls.return_value
llm = LLM.with_nebius(model=_MODEL)

assert llm.model == _MODEL
assert client_cls.call_args.kwargs["api_key"] == "env-key"
assert str(client_cls.call_args.kwargs["base_url"]) == _BASE_URL
assert client is llm._client


async def test_nebius_helper_sends_request_to_configured_client() -> None:
transport = _RecordingTransport()
llm = LLM.with_nebius(
model=_MODEL,
api_key="test-key",
base_url=_BASE_URL,
client=None,
)
await llm._client.close()
llm._client = openai.AsyncClient(
api_key="test-key",
base_url=_BASE_URL,
http_client=httpx.AsyncClient(transport=transport),
)
chat_ctx = ChatContext.empty()
chat_ctx.add_message(role="user", content="hello")

stream = llm.chat(chat_ctx=chat_ctx)
try:
async for _ in stream:
pass
finally:
await stream.aclose()
await llm.aclose()

assert len(transport.requests) == 1
request = transport.requests[0]
assert str(request.url) == f"{_BASE_URL}chat/completions"
payload = json.loads(request.content)
assert payload["model"] == _MODEL
assert payload["messages"] == [{"role": "user", "content": "hello"}]