From 2bef65c35c2643025c30c2e17c4fa2fa055c1a71 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Wed, 18 Mar 2026 21:51:24 +0800 Subject: [PATCH] feat: add Novita AI as a native LLM provider Add native support for Novita AI's OpenAI-compatible API (https://api.novita.ai/openai) as a first-class LLM provider. - New NovitaCompletion class extending OpenAICompletion - API key via NOVITA_API_KEY env var or constructor param - Supported models: moonshotai/kimi-k2.5 (default), zai-org/glm-5, minimax/minimax-m2.5 - Auto-detection of vendor/model IDs (e.g. "moonshotai/kimi-k2.5") routes to Novita without requiring "novita/" prefix Co-Authored-By: Claude Opus 4.6 --- lib/crewai/src/crewai/llm.py | 27 ++++++ lib/crewai/src/crewai/llms/constants.py | 12 +++ .../crewai/llms/providers/novita/__init__.py | 0 .../llms/providers/novita/completion.py | 89 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 lib/crewai/src/crewai/llms/providers/novita/__init__.py create mode 100644 lib/crewai/src/crewai/llms/providers/novita/completion.py diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 8a4ac2edde..7d600aece5 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -43,6 +43,7 @@ AZURE_MODELS, BEDROCK_MODELS, GEMINI_MODELS, + NOVITA_MODELS, OPENAI_MODELS, ) from crewai.utilities import InternalInstructor @@ -311,6 +312,10 @@ def writable(self) -> bool: "mistral/mistral-large-latest": 32768, "mistral/mistral-large-2407": 32768, "mistral/mistral-large-2402": 32768, + # novita + "moonshotai/kimi-k2.5": 131072, + "zai-org/glm-5": 131072, + "minimax/minimax-m2.5": 131072, } DEFAULT_CONTEXT_WINDOW_SIZE: Final[int] = 8192 @@ -325,6 +330,7 @@ def writable(self) -> bool: "gemini", "bedrock", "aws", + "novita", ] @@ -384,6 +390,7 @@ def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM: "gemini": "gemini", "bedrock": "bedrock", "aws": "bedrock", + "novita": "novita", } canonical_provider = provider_mapping.get(prefix.lower()) @@ -394,6 +401,12 @@ def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM: provider = canonical_provider use_native = True model_string = model_part + elif model in NOVITA_MODELS: + # Handle vendor/model IDs (e.g. "moonshotai/kimi-k2.5") + # that are Novita models but whose prefix is a vendor, not a provider + provider = "novita" + use_native = True + model_string = model else: provider = prefix use_native = False @@ -483,6 +496,9 @@ def _matches_provider_pattern(cls, model: str, provider: str) -> bool: for prefix in ["gpt-", "gpt-35-", "o1", "o3", "o4", "azure-"] ) + if provider == "novita": + return model in NOVITA_MODELS + return False @classmethod @@ -518,6 +534,9 @@ def _validate_model_in_constants(cls, model: str, provider: str) -> bool: # azure does not provide a list of available models, determine a better way to handle this return True + if provider == "novita" and model in NOVITA_MODELS: + return True + # Fallback to pattern matching for models not in constants return cls._matches_provider_pattern(model, provider) @@ -547,6 +566,9 @@ def _infer_provider_from_model(cls, model: str) -> str: if model in BEDROCK_MODELS: return "bedrock" + if model in NOVITA_MODELS: + return "novita" + if model in AZURE_MODELS: return "azure" @@ -582,6 +604,11 @@ def _get_native_provider(cls, provider: str) -> type | None: return BedrockCompletion + if provider == "novita": + from crewai.llms.providers.novita.completion import NovitaCompletion + + return NovitaCompletion + return None def __init__( diff --git a/lib/crewai/src/crewai/llms/constants.py b/lib/crewai/src/crewai/llms/constants.py index 595a0a30dd..41542c1360 100644 --- a/lib/crewai/src/crewai/llms/constants.py +++ b/lib/crewai/src/crewai/llms/constants.py @@ -1,6 +1,18 @@ from typing import Literal, TypeAlias +NovitaModels: TypeAlias = Literal[ + "moonshotai/kimi-k2.5", + "zai-org/glm-5", + "minimax/minimax-m2.5", +] +NOVITA_MODELS: list[NovitaModels] = [ + "moonshotai/kimi-k2.5", + "zai-org/glm-5", + "minimax/minimax-m2.5", +] + + OpenAIModels: TypeAlias = Literal[ "gpt-3.5-turbo", "gpt-3.5-turbo-0125", diff --git a/lib/crewai/src/crewai/llms/providers/novita/__init__.py b/lib/crewai/src/crewai/llms/providers/novita/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai/src/crewai/llms/providers/novita/completion.py b/lib/crewai/src/crewai/llms/providers/novita/completion.py new file mode 100644 index 0000000000..4e4b682200 --- /dev/null +++ b/lib/crewai/src/crewai/llms/providers/novita/completion.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Any + +from crewai.llms.providers.openai.completion import OpenAICompletion + +if TYPE_CHECKING: + import httpx + + from crewai.llms.hooks.base import BaseInterceptor + +NOVITA_BASE_URL = "https://api.novita.ai/openai" +NOVITA_DEFAULT_MODEL = "moonshotai/kimi-k2.5" + + +class NovitaCompletion(OpenAICompletion): + """Novita AI completion implementation. + + Uses the OpenAI-compatible endpoint at https://api.novita.ai/openai. + Inherits all functionality from OpenAICompletion. + """ + + def __init__( + self, + model: str = NOVITA_DEFAULT_MODEL, + api_key: str | None = None, + base_url: str | None = None, + timeout: float | None = None, + max_retries: int = 2, + temperature: float | None = None, + provider: str | None = None, + interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None, + **kwargs: Any, + ) -> None: + """Initialize Novita AI completion client. + + Args: + model: Model ID, defaults to moonshotai/kimi-k2.5. + api_key: Novita API key. Falls back to NOVITA_API_KEY env var. + base_url: Override base URL. Defaults to https://api.novita.ai/openai. + **kwargs: Passed through to OpenAICompletion. + """ + resolved_api_key = api_key or os.getenv("NOVITA_API_KEY") + if not resolved_api_key: + raise ValueError( + "Novita AI API key is required. " + "Set the NOVITA_API_KEY environment variable or pass api_key." + ) + + super().__init__( + model=model, + api_key=resolved_api_key, + base_url=base_url or NOVITA_BASE_URL, + timeout=timeout, + max_retries=max_retries, + temperature=temperature, + provider=provider or "novita", + interceptor=interceptor, + # Force completions API — Responses API is OpenAI-specific + api="completions", + **kwargs, + ) + + def _get_client_params(self) -> dict[str, Any]: + """Get client parameters with Novita AI defaults.""" + if self.api_key is None: + self.api_key = os.getenv("NOVITA_API_KEY") + if self.api_key is None: + raise ValueError( + "Novita AI API key is required. " + "Set the NOVITA_API_KEY environment variable or pass api_key." + ) + + base_params = { + "api_key": self.api_key, + "base_url": self.base_url or NOVITA_BASE_URL, + "timeout": self.timeout, + "max_retries": self.max_retries, + "default_headers": self.default_headers, + "default_query": self.default_query, + } + + client_params = {k: v for k, v in base_params.items() if v is not None} + + if self.client_params: + client_params.update(self.client_params) + + return client_params