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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ GOOGLE_API_KEY=
GOOGLE_GENAI_USE_VERTEXAI=false
GOOGLE_CLOUD_PROJECT=
MINIMAX_API_KEY=
NOVITA_API_KEY=

# OpenAI Compatible Provider
OPENAI_COMPATIBLE_API_KEY=
Expand Down
77 changes: 38 additions & 39 deletions LLM.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
# IntentKit LLM Guide

## Architecture

- `intentkit/` — pip package
- `core/` — agent system (LangGraph)
- `manager/` — single agent manager
- `system_skills/` — built-in system skills
- `models/` — Pydantic + SQLAlchemy dual models
- `config/` — system config (DB, LLM keys, skill provider keys)
- `skills/` — skill system (LangChain BaseTool)
- `abstracts/` — interfaces for core/ and skills/
- `utils/` — utilities
- `clients/` — external service clients
- `app/` — API server, autonomous runner, background scheduler
- `frontend/` — Next.js agent management UI (see `frontend/AGENTS.md`)
- `integrations/` — platform integrations (each has its own `AGENTS.md`)
- `telegram/` — Telegram bot integration
- `scripts/` — ops & migration scripts
- `tests/` — `tests/core/`, `tests/api/`, `tests/skills/`

## Tech Stack & Gotchas

- Package manager: **uv**. Activate venv: `source .venv/bin/activate`
- Lint: `ruff format & ruff check --fix` after edits
- Type check: **BasedPyright** — ensure no errors in changed files
- **SQLAlchemy 2.0** — do NOT use legacy 1.x API
- **Pydantic V2** — do NOT use V1 API
- Testing: **pytest**

## Rules
- Do NOT add "Generated with Claude Code", "Co-Authored-By: Claude", or any AI-attribution footer to PR descriptions, commit messages, or code comments.
- Do NOT use the /commit or /pr slash commands. Use git and gh CLI directly.

# Novita AI Model Catalog

Use ONLY these model IDs. Do NOT invent or guess model IDs.

## API Endpoints

Novita AI supports two API-compatible endpoints. Choose based on which SDK the target repo uses:

| SDK / Interface | Base URL | Env Var |
|----------------|----------|---------|
| **OpenAI-compatible** | `https://api.novita.ai/openai` | `OPENAI_BASE_URL` or `OPENAI_API_BASE` |
| **Anthropic-compatible** | `https://api.novita.ai/anthropic` | `ANTHROPIC_BASE_URL` |

- API key for both: `NOVITA_API_KEY` (or follow the repo's existing env var pattern).
- If the repo uses the **OpenAI SDK** (`openai`, `langchain`, etc.) → use the OpenAI endpoint.
- If the repo uses the **Anthropic SDK** (`anthropic`, `@anthropic-ai/sdk`, etc.) → use the Anthropic endpoint.
- If the repo supports **both**, add both endpoints.

## Chat / Completion Models

| Model ID | Context | Max Output | Architecture | Features | Pricing (per M tokens) |
|----------|---------|------------|--------------|----------|----------------------|
| `moonshotai/kimi-k2.5` **(default)** | 262,144 | 262,144 | MoE | function_calling, structured_output, reasoning, vision | In: $0.6 / Out: $3 / Cache: $0.1 |
| `zai-org/glm-5` | 202,800 | 131,072 | MoE | function_calling, structured_output, reasoning | In: $1 / Out: $3.2 / Cache: $0.2 |
| `minimax/minimax-m2.5` | 204,800 | 131,100 | MoE | function_calling, structured_output, reasoning | In: $0.3 / Out: $1.2 / Cache: $0.03 |

## Embedding Models

- English for code comments and search queries
- Do not git commit unless explicitly asked
- Import dependency order (left cannot import right): `utils → config → models → abstracts → clients → skills → core`
- **No ForeignKey constraints**: All tables intentionally omit `ForeignKey` constraints. Do NOT add FK constraints to any table definition.
- **AgentCore ↔ Template sync**: `AgentCore` (Pydantic) is the shared base for both `Agent` and `Template`. When adding/removing fields in `AgentCore`, you MUST also update `TemplateTable` (SQLAlchemy columns in `intentkit/models/template.py`) to match. The `Template` Pydantic model inherits from `AgentCore` automatically, but the DB schema does not. Agent-specific fields like `slug` belong in `AgentUserInput`, not `AgentCore`.
| Model ID | Dimensions | Max Input | Pricing |
|----------|-----------|-----------|---------|
| `qwen/qwen3-embedding-0.6b` | 1024 | 8,192 | $0.014 / M tokens |

## Detailed Guides
## Selection Rules

- Skills: `agent_docs/skill_development.md`
- Git/PR/Release: `agent_docs/ops_guide.md`
- Testing: `agent_docs/test.md`
- If only one model slot: use `moonshotai/kimi-k2.5`
- If multiple model slots: default `moonshotai/kimi-k2.5`, also add `zai-org/glm-5` and `minimax/minimax-m2.5`
- If embedding slot: use `qwen/qwen3-embedding-0.6b`
- If vision/multimodal slot: use `moonshotai/kimi-k2.5` (supports image + video input)
1 change: 1 addition & 0 deletions intentkit/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def __init__(self) -> None:
self.xai_api_key: str | None = self.load("XAI_API_KEY")
self.minimax_api_key: str | None = self.load("MINIMAX_API_KEY")
self.openrouter_api_key: str | None = self.load("OPENROUTER_API_KEY")
self.novita_api_key: str | None = self.load("NOVITA_API_KEY")
# OpenAI Compatible provider
self.openai_compatible_api_key: str | None = self.load(
"OPENAI_COMPATIBLE_API_KEY"
Expand Down
3 changes: 3 additions & 0 deletions intentkit/models/llm.csv
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ x-ai/grok-4.1-fast,Grok 4.1 Fast,openrouter,TRUE,0.2,0.05,0.5,2,2000000,4096,3,4
grok-4-1-fast-reasoning,Grok 4.1 Fast Reasoning,xai,TRUE,0.2,0.05,0.5,2,2000000,4096,3,4,TRUE,high,TRUE,FALSE,FALSE,180
x-ai/grok-4.1-fast-reasoning,Grok 4.1 Fast Reasoning,openrouter,TRUE,0.2,0.05,0.5,2,2000000,4096,3,4,TRUE,high,TRUE,TRUE,TRUE,180
MiniMax-M2.7,MiniMax M2.7,minimax,TRUE,0.1,0.01,0.4,1,204800,131072,5,3,FALSE,high,TRUE,TRUE,TRUE,300
moonshotai/kimi-k2.5,Kimi K2.5,novita,TRUE,0.6,0.1,3,2,262144,262144,4,3,TRUE,high,TRUE,TRUE,TRUE,300
zai-org/glm-5,GLM 5,novita,TRUE,1,0.2,3.2,3,202800,131072,5,2,FALSE,high,TRUE,TRUE,TRUE,300
minimax/minimax-m2.5,MiniMax M2.5,novita,TRUE,0.3,0.03,1.2,2,204800,131100,5,3,FALSE,high,TRUE,TRUE,TRUE,300
39 changes: 39 additions & 0 deletions intentkit/models/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class LLMProvider(str, Enum):
MINIMAX = "minimax"
OLLAMA = "ollama"
OPENAI_COMPATIBLE = "openai_compatible"
NOVITA = "novita"

@property
def is_configured(self) -> bool:
Expand All @@ -207,6 +208,7 @@ def is_configured(self) -> bool:
and config.openai_compatible_base_url
and config.openai_compatible_model
),
self.NOVITA: bool(config.novita_api_key),
}
return config_map.get(self, False)

Expand All @@ -221,6 +223,7 @@ def display_name(self) -> str:
self.MINIMAX: "MiniMax",
self.OLLAMA: "Ollama",
self.OPENAI_COMPATIBLE: config.openai_compatible_provider,
self.NOVITA: "Novita AI",
}
return display_names.get(self, self.value)

Expand Down Expand Up @@ -929,6 +932,41 @@ async def create_instance(self, params: dict[str, Any] = {}) -> BaseChatModel:
return ChatOpenAI(**kwargs)


class NovitaLLM(LLMModel):
"""Novita AI LLM configuration (OpenAI-compatible endpoint)."""

@override
async def create_instance(self, params: dict[str, Any] = {}) -> BaseChatModel:
"""Create and return a ChatOpenAI instance for Novita AI."""
from langchain_openai import ChatOpenAI

info = await self.model_info()

kwargs: dict[str, Any] = {
"model_name": info.id,
"openai_api_key": config.novita_api_key,
"openai_api_base": "https://api.novita.ai/openai",
"timeout": info.timeout,
"max_retries": 3,
}

if info.supports_temperature:
kwargs["temperature"] = self.temperature

if info.supports_frequency_penalty:
kwargs["frequency_penalty"] = self.frequency_penalty

if info.supports_presence_penalty:
kwargs["presence_penalty"] = self.presence_penalty

if info.reasoning_effort and info.reasoning_effort != "none":
kwargs["reasoning_effort"] = info.reasoning_effort

kwargs.update(params)

return ChatOpenAI(**kwargs)


# Factory function to create the appropriate LLM model based on the model name
async def create_llm_model(
model_name: str,
Expand Down Expand Up @@ -959,6 +997,7 @@ async def create_llm_model(
LLMProvider.OPENAI: OpenAILLM,
LLMProvider.MINIMAX: MiniMaxLLM,
LLMProvider.OPENAI_COMPATIBLE: OpenAICompatibleLLM,
LLMProvider.NOVITA: NovitaLLM,
}

model_class = provider_map.get(info.provider, OpenAILLM)
Expand Down
1 change: 1 addition & 0 deletions intentkit/models/llm_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def pick_default_model() -> str:
# 4. Grok: Good performance if available
# 5. DeepSeek: Final fallback
order: list[tuple[str, LLMProvider]] = [
("moonshotai/kimi-k2.5", LLMProvider.NOVITA),
("MiniMax-M2.7", LLMProvider.MINIMAX),
("minimax/minimax-m2.7", LLMProvider.OPENROUTER),
("google/gemini-3-flash-preview", LLMProvider.GOOGLE),
Expand Down
7 changes: 7 additions & 0 deletions tests/core/test_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def test_llm_model_filtering():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand All @@ -34,6 +35,7 @@ def test_llm_model_filtering():
LLMProvider.ETERNAL,
LLMProvider.REIGENT,
LLMProvider.VENICE,
LLMProvider.NOVITA,
}

for model in models.values():
Expand All @@ -53,6 +55,7 @@ def test_llm_model_filtering():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand All @@ -79,6 +82,7 @@ def test_llm_model_filtering():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand All @@ -102,6 +106,7 @@ def test_llm_model_filtering():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand Down Expand Up @@ -129,6 +134,7 @@ def test_llm_model_filtering():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand Down Expand Up @@ -159,6 +165,7 @@ def test_model_id_index_suffix_matching():
mock_config.reigent_api_key = None
mock_config.venice_api_key = None
mock_config.minimax_api_key = None
mock_config.novita_api_key = None
mock_config.openai_compatible_api_key = None
mock_config.openai_compatible_base_url = None
mock_config.openai_compatible_model = None
Expand Down