Skip to content

Commit 01944b7

Browse files
committed
feat: add Novita AI as LLM provider
Add Novita AI integration with OpenAI-compatible API support: - Add NovitaChat class using OpenAI SDK - Add resolve_novita_base_url and resolve_novita_api_key settings - Add 'novita' to CLI --llm choices (ask and react commands) - Add provider tests following minimax pattern Default model: moonshotai/kimi-k2.5 Base URL: https://api.novita.ai/openai API key env var: NOVITA_API_KEY
1 parent 3cab4ef commit 01944b7

4 files changed

Lines changed: 349 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+
Supported models (from Novita AI catalog):
1019+
- moonshotai/kimi-k2.5 (default): 262K context, MoE architecture
1020+
- zai-org/glm-5: 202K context, MoE with function calling
1021+
- minimax/minimax-m2.5: 204K context, MoE with reasoning
1022+
1023+
All models support function calling, structured output, and reasoning.
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: 14 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(
@@ -2719,6 +2721,11 @@ async def ask_questions(self, args):
27192721
resolved_api_key = resolve_minimax_api_key(args.api_key)
27202722
if resolved_api_key:
27212723
llm_config["api_key"] = resolved_api_key
2724+
elif args.llm == "novita":
2725+
llm_config["base_url"] = resolve_novita_base_url(args.api_base)
2726+
resolved_api_key = resolve_novita_api_key(args.api_key)
2727+
if resolved_api_key:
2728+
llm_config["api_key"] = resolved_api_key
27222729

27232730
chat = LeannChat(index_path=index_path, llm_config=llm_config)
27242731

@@ -2816,6 +2823,11 @@ async def react_agent(self, args):
28162823
resolved_api_key = resolve_minimax_api_key(args.api_key)
28172824
if resolved_api_key:
28182825
llm_config["api_key"] = resolved_api_key
2826+
elif args.llm == "novita":
2827+
llm_config["base_url"] = resolve_novita_base_url(args.api_base)
2828+
resolved_api_key = resolve_novita_api_key(args.api_key)
2829+
if resolved_api_key:
2830+
llm_config["api_key"] = resolved_api_key
28192831

28202832
from .react_agent import create_react_agent
28212833

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

Lines changed: 28 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,33 @@ 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)."""
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+
os.getenv("OPENAI_API_BASE"),
127+
)
128+
129+
for candidate in candidates:
130+
if candidate:
131+
return _clean_url(candidate)
132+
133+
return _clean_url(_DEFAULT_NOVITA_BASE_URL)
134+
135+
136+
def resolve_novita_api_key(explicit: str | None = None) -> str | None:
137+
"""Resolve the API key for Novita AI services."""
138+
139+
if explicit:
140+
return explicit
141+
142+
return os.getenv("NOVITA_API_KEY")
143+
144+
117145
def encode_provider_options(options: dict[str, Any] | None) -> str | None:
118146
"""Serialize provider options for child processes."""
119147

0 commit comments

Comments
 (0)