Skip to content

Commit c574e32

Browse files
committed
feat: add Novita AI as LLM provider
Add NovitaChat class supporting Novita AI's OpenAI-compatible endpoint. Default model is moonshotai/kimi-k2.5 with 262K context window. Integration points: - NovitaChat class in chat.py (OpenAI SDK compatible) - resolve_novita_base_url/resolve_novita_api_key in settings.py - "novita" added to CLI --llm choices - get_llm factory supports "novita" type - test_novita_provider.py with unit and live API tests Environment variables: - NOVITA_API_KEY: API key (required) - NOVITA_BASE_URL: Custom endpoint (optional, defaults to api.novita.ai/openai)
1 parent 3cab4ef commit c574e32

4 files changed

Lines changed: 343 additions & 2 deletions

File tree

packages/leann-core/src/leann/chat.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
resolve_anthropic_base_url,
1818
resolve_minimax_api_key,
1919
resolve_minimax_base_url,
20+
resolve_novita_api_key,
21+
resolve_novita_base_url,
2022
resolve_ollama_host,
2123
resolve_openai_api_key,
2224
resolve_openai_base_url,
@@ -1010,6 +1012,73 @@ def ask(self, prompt: str, **kwargs) -> str:
10101012
return f"Error: Could not get a response from MiniMax. Details: {e}"
10111013

10121014

1015+
class NovitaChat(LLMInterface):
1016+
"""LLM interface for Novita AI models via the OpenAI-compatible API.
1017+
1018+
Novita AI provides access to various LLM models including:
1019+
- moonshotai/kimi-k2.5 (default): MoE model with 262K context
1020+
- zai-org/glm-5: MoE model with 202K context
1021+
- minimax/minimax-m2.5: MoE model with 204K context
1022+
1023+
The OpenAI-compatible endpoint is used for chat completions.
1024+
"""
1025+
1026+
def __init__(
1027+
self,
1028+
model: str = "moonshotai/kimi-k2.5",
1029+
api_key: Optional[str] = None,
1030+
base_url: Optional[str] = None,
1031+
):
1032+
self.model = model
1033+
self.base_url = resolve_novita_base_url(base_url)
1034+
self.api_key = resolve_novita_api_key(api_key)
1035+
1036+
if not self.api_key:
1037+
raise ValueError(
1038+
"Novita AI API key is required. Set NOVITA_API_KEY environment variable or pass api_key parameter."
1039+
)
1040+
1041+
logger.info(
1042+
"Initializing Novita Chat with model='%s' and base_url='%s'",
1043+
model,
1044+
self.base_url,
1045+
)
1046+
1047+
try:
1048+
import openai
1049+
1050+
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
1051+
except ImportError:
1052+
raise ImportError(
1053+
"The 'openai' library is required for Novita AI models. Please install it with 'pip install openai'."
1054+
)
1055+
1056+
def ask(self, prompt: str, **kwargs) -> str:
1057+
params = {
1058+
"model": self.model,
1059+
"messages": [{"role": "user", "content": prompt}],
1060+
"temperature": kwargs.get("temperature", 0.7),
1061+
"max_tokens": kwargs.get("max_tokens", 1000),
1062+
}
1063+
1064+
if "top_p" in kwargs:
1065+
params["top_p"] = kwargs["top_p"]
1066+
1067+
logger.info(f"Sending request to Novita AI with model {self.model}")
1068+
1069+
try:
1070+
response = cast(Any, self.client.chat.completions).create(**params)
1071+
print(
1072+
f"Total tokens = {response.usage.total_tokens}, prompt tokens = {response.usage.prompt_tokens}, completion tokens = {response.usage.completion_tokens}"
1073+
)
1074+
if response.choices[0].finish_reason == "length":
1075+
print("The query is exceeding the maximum allowed number of tokens")
1076+
return response.choices[0].message.content.strip()
1077+
except Exception as e:
1078+
logger.error(f"Error communicating with Novita AI: {e}")
1079+
return f"Error: Could not get a response from Novita AI. Details: {e}"
1080+
1081+
10131082
class SimulatedChat(LLMInterface):
10141083
"""A simple simulated chat for testing and development."""
10151084

@@ -1074,6 +1143,12 @@ def get_llm(llm_config: Optional[dict[str, Any]] = None) -> LLMInterface:
10741143
api_key=llm_config.get("api_key"),
10751144
base_url=llm_config.get("base_url"),
10761145
)
1146+
elif llm_type == "novita":
1147+
return NovitaChat(
1148+
model=model or "moonshotai/kimi-k2.5",
1149+
api_key=llm_config.get("api_key"),
1150+
base_url=llm_config.get("base_url"),
1151+
)
10771152
elif llm_type == "simulated":
10781153
return SimulatedChat()
10791154
else:

packages/leann-core/src/leann/cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
resolve_anthropic_base_url,
2525
resolve_minimax_api_key,
2626
resolve_minimax_base_url,
27+
resolve_novita_api_key,
28+
resolve_novita_base_url,
2729
resolve_ollama_host,
2830
resolve_openai_api_key,
2931
resolve_openai_base_url,
@@ -495,7 +497,7 @@ def create_parser(self) -> argparse.ArgumentParser:
495497
"--llm",
496498
type=str,
497499
default="ollama",
498-
choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax"],
500+
choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax", "novita"],
499501
help="LLM provider (default: ollama)",
500502
)
501503
ask_parser.add_argument(
@@ -558,7 +560,7 @@ def create_parser(self) -> argparse.ArgumentParser:
558560
"--llm",
559561
type=str,
560562
default="ollama",
561-
choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax"],
563+
choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax", "novita"],
562564
help="LLM provider (default: ollama)",
563565
)
564566
react_parser.add_argument(

packages/leann-core/src/leann/settings.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
_DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
1212
_DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
1313
_DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1"
14+
_DEFAULT_NOVITA_BASE_URL = "https://api.novita.ai/openai"
1415

1516

1617
def _clean_url(value: str) -> str:
@@ -114,6 +115,32 @@ def resolve_minimax_api_key(explicit: str | None = None) -> str | None:
114115
return os.getenv("MINIMAX_API_KEY")
115116

116117

118+
def resolve_novita_base_url(explicit: str | None = None) -> str:
119+
"""Resolve the base URL for Novita AI services (OpenAI-compatible endpoint)."""
120+
121+
candidates = (
122+
explicit,
123+
os.getenv("LEANN_NOVITA_BASE_URL"),
124+
os.getenv("NOVITA_BASE_URL"),
125+
os.getenv("OPENAI_BASE_URL"),
126+
)
127+
128+
for candidate in candidates:
129+
if candidate:
130+
return _clean_url(candidate)
131+
132+
return _clean_url(_DEFAULT_NOVITA_BASE_URL)
133+
134+
135+
def resolve_novita_api_key(explicit: str | None = None) -> str | None:
136+
"""Resolve the API key for Novita AI services."""
137+
138+
if explicit:
139+
return explicit
140+
141+
return os.getenv("NOVITA_API_KEY")
142+
143+
117144
def encode_provider_options(options: dict[str, Any] | None) -> str | None:
118145
"""Serialize provider options for child processes."""
119146

0 commit comments

Comments
 (0)