Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

166 lines
5.4 KiB
Python

"""Tests for the Novita LLM provider.
Novita uses an OpenAI-compatible API, so NovitaLLM extends OpenAILLM.
These tests verify the Novita-specific configuration is applied correctly.
"""
import types
from unittest.mock import patch
import pytest
from application.llm.novita import NOVITA_BASE_URL, NovitaLLM
class FakeChatCompletions:
"""Fake OpenAI chat completions for testing."""
def __init__(self):
self.last_kwargs = None
class _Msg:
def __init__(self, content=None):
self.content = content
class _Delta:
def __init__(self, content=None):
self.content = content
class _Choice:
def __init__(self, content=None, delta=None):
self.message = FakeChatCompletions._Msg(content=content)
self.delta = FakeChatCompletions._Delta(content=delta)
class _StreamChunk:
def __init__(self, choice):
self.choices = [choice]
class _Response:
def __init__(self, choices=None, lines=None):
self._choices = choices or []
self._lines = lines or []
@property
def choices(self):
return self._choices
def __iter__(self):
for line in self._lines:
yield line
def create(self, **kwargs):
self.last_kwargs = kwargs
if not kwargs.get("stream"):
return FakeChatCompletions._Response(choices=[FakeChatCompletions._Choice(content="novita response")])
return FakeChatCompletions._Response(
lines=[
FakeChatCompletions._StreamChunk(FakeChatCompletions._Choice(delta="part1")),
FakeChatCompletions._StreamChunk(FakeChatCompletions._Choice(delta="part2")),
]
)
class FakeClient:
"""Fake OpenAI client for testing."""
def __init__(self):
self.chat = types.SimpleNamespace(completions=FakeChatCompletions())
@pytest.mark.unit
def test_novita_base_url_constant():
"""Verify the Novita base URL is correctly defined."""
assert NOVITA_BASE_URL == "https://api.novita.ai/openai"
@pytest.mark.unit
def test_novita_llm_uses_novita_base_url():
"""Verify NovitaLLM uses the Novita API endpoint."""
llm = NovitaLLM(api_key="test-key", user_api_key=None)
# The client should be configured with Novita's base URL
assert str(llm.client.base_url) == NOVITA_BASE_URL + "/"
@pytest.mark.unit
def test_novita_llm_uses_novita_api_key():
"""Verify NovitaLLM prioritizes NOVITA_API_KEY from settings."""
with patch("application.llm.novita.settings") as mock_settings:
mock_settings.NOVITA_API_KEY = "novita-test-key"
mock_settings.API_KEY = "fallback-key"
mock_settings.OPENAI_BASE_URL = None
llm = NovitaLLM(api_key=None, user_api_key=None)
assert llm.api_key == "novita-test-key"
@pytest.mark.unit
def test_novita_llm_falls_back_to_api_key():
"""Verify NovitaLLM falls back to API_KEY when NOVITA_API_KEY is not set."""
with patch("application.llm.novita.settings") as mock_settings:
mock_settings.NOVITA_API_KEY = None
mock_settings.API_KEY = "fallback-key"
mock_settings.OPENAI_BASE_URL = None
llm = NovitaLLM(api_key=None, user_api_key=None)
assert llm.api_key == "fallback-key"
@pytest.mark.unit
def test_novita_llm_explicit_api_key_takes_precedence():
"""Verify explicitly passed API key takes precedence over settings."""
with patch("application.llm.novita.settings") as mock_settings:
mock_settings.NOVITA_API_KEY = "settings-key"
mock_settings.API_KEY = "fallback-key"
mock_settings.OPENAI_BASE_URL = None
llm = NovitaLLM(api_key="explicit-key", user_api_key=None)
assert llm.api_key == "explicit-key"
@pytest.mark.unit
def test_novita_llm_custom_base_url():
"""Verify custom base_url can override the default Novita URL."""
custom_url = "https://custom.novita.endpoint/v1"
llm = NovitaLLM(api_key="test-key", user_api_key=None, base_url=custom_url)
assert str(llm.client.base_url) == custom_url + "/"
@pytest.mark.unit
def test_novita_llm_supports_tools():
"""Verify NovitaLLM supports function calling/tools."""
llm = NovitaLLM(api_key="test-key", user_api_key=None)
assert llm.supports_tools() is True
@pytest.mark.unit
def test_novita_llm_supports_structured_output():
"""Verify NovitaLLM supports structured output."""
llm = NovitaLLM(api_key="test-key", user_api_key=None)
assert llm.supports_structured_output() is True
@pytest.mark.unit
def test_novita_llm_gen_calls_client(monkeypatch):
"""Verify NovitaLLM.gen calls the OpenAI-compatible client correctly."""
llm = NovitaLLM(api_key="test-key", user_api_key=None)
llm.client = FakeClient()
msgs = [{"role": "user", "content": "hello"}]
result = llm._raw_gen(llm, model="moonshotai/kimi-k2.5", messages=msgs, stream=False)
assert result == "novita response"
assert llm.client.chat.completions.last_kwargs["model"] == "moonshotai/kimi-k2.5"
@pytest.mark.unit
def test_novita_llm_gen_stream_yields_chunks(monkeypatch):
"""Verify NovitaLLM streaming yields chunks correctly."""
llm = NovitaLLM(api_key="test-key", user_api_key=None)
llm.client = FakeClient()
msgs = [{"role": "user", "content": "hi"}]
gen = llm._raw_gen_stream(llm, model="moonshotai/kimi-k2.5", messages=msgs, stream=True)
chunks = list(gen)
assert "part1" in "".join(chunks)
assert "part2" in "".join(chunks)