Skip to content
Open
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
27 changes: 27 additions & 0 deletions lib/crewai/src/crewai/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
AZURE_MODELS,
BEDROCK_MODELS,
GEMINI_MODELS,
NOVITA_MODELS,
OPENAI_MODELS,
)
from crewai.utilities import InternalInstructor
Expand Down Expand Up @@ -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
Expand All @@ -325,6 +330,7 @@ def writable(self) -> bool:
"gemini",
"bedrock",
"aws",
"novita",
]


Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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__(
Expand Down
12 changes: 12 additions & 0 deletions lib/crewai/src/crewai/llms/constants.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Empty file.
89 changes: 89 additions & 0 deletions lib/crewai/src/crewai/llms/providers/novita/completion.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Novita models get wrong context window size

Medium Severity

NovitaCompletion inherits get_context_window_size() from OpenAICompletion, which only checks OpenAI-specific model prefixes (e.g. gpt-4, o1). Since Novita models like moonshotai/kimi-k2.5 don't match any OpenAI prefix, the method returns the default ~6,963 tokens instead of the intended ~111,411 tokens (131072 × 0.85). The entries added to LLM_CONTEXT_WINDOW_SIZES are dead code here — that dict is only consumed by the LLM (litellm) class's override, not by native provider classes. Every other native provider (Anthropic, Gemini, Bedrock) overrides get_context_window_size() with its own model-specific windows; NovitaCompletion needs the same.

Additional Locations (1)
Fix in Cursor Fix in Web