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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ ATLAS_API_KEY=
# ATLAS_BASE_URL=https://api.atlascloud.ai/v1
# ATLAS_MODEL=anthropic/claude-sonnet-4.6

# --- Optional: MiniMax (OpenAI- and Anthropic-compatible) -------------------
# OpenAI SDK: global https://api.minimax.io/v1
# OpenAI SDK: China https://api.minimaxi.com/v1
# Anthropic SDK: global https://api.minimax.io/anthropic
# Anthropic SDK: China https://api.minimaxi.com/anthropic
# Select with LLM_PROVIDER=minimax or call get_provider("minimax").
MINIMAX_API_KEY=
# MINIMAX_MODEL=MiniMax-M3 # Also supports MiniMax-M2.7
# MINIMAX_BASE_URL=https://api.minimax.io/v1

# ─── ECC agent data (multi-harness isolation) ───────────────────────────────
# Memory hooks (sessions, learned skills, aliases, metrics). Default: ~/.claude
# Use a separate root when running ECC in Cursor alongside Claude Code:
Expand Down
5 changes: 5 additions & 0 deletions src/llm/cli/selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def interactive_select(
if providers is None:
providers = [
("claude", "Anthropic Claude ( Sonnet, Opus, Haiku)"),
("minimax", "MiniMax (M3, M2.7)"),
("openai", "OpenAI GPT (4o, 4o-mini, 3.5-turbo)"),
("ollama", "Local Ollama models"),
]
Expand All @@ -109,6 +110,10 @@ def interactive_select(
("claude-sonnet-4-6", "Claude Sonnet 4.6 - Balanced"),
("claude-haiku-4-5", "Claude Haiku 4.5 - Fast"),
],
"minimax": [
("MiniMax-M3", "MiniMax M3 - Long-context multimodal model"),
("MiniMax-M2.7", "MiniMax M2.7 - Long-context text model"),
],
"openai": [
("gpt-4o", "GPT-4o - Most capable"),
("gpt-4o-mini", "GPT-4o-mini - Fast & affordable"),
Expand Down
4 changes: 3 additions & 1 deletion src/llm/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ class ProviderType(str, Enum):
ASTRAFLOW = "astraflow"
ASTRAFLOW_CN = "astraflow_cn"
ATLAS = "atlas"
MINIMAX = "minimax"


@dataclass(frozen=True)
class Message:
role: Role
content: str
content: str | list[dict[str, Any]]
name: str | None = None
tool_call_id: str | None = None
tool_calls: list[ToolCall] | None = None
metadata: dict[str, Any] = field(default_factory=dict)

def to_dict(self) -> dict[str, Any]:
result: dict[str, Any] = {"role": self.role.value, "content": self.content}
Expand Down
27 changes: 15 additions & 12 deletions src/llm/prompt/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
from dataclasses import dataclass
from typing import Any

from llm.core.types import LLMInput, Message, Role, ToolDefinition
from llm.providers.claude import ClaudeProvider
from llm.providers.openai import OpenAIProvider
from llm.providers.ollama import OllamaProvider
from llm.core.types import Message, Role, ToolDefinition


@dataclass
Expand Down Expand Up @@ -36,13 +33,16 @@ def __init__(
raise ValueError("Pass either config or PromptBuilder keyword options, not both")

if config is None:
overrides = {
"system_template": system_template,
"user_template": user_template,
"include_tools_in_system": include_tools_in_system,
"tool_format": tool_format,
}
config = PromptConfig(**{key: value for key, value in overrides.items() if value is not None})
config = PromptConfig(
system_template=system_template,
user_template=user_template,
include_tools_in_system=(
include_tools_in_system
if include_tools_in_system is not None
else True
),
tool_format=tool_format if tool_format is not None else "native",
)

self.config = config

Expand All @@ -61,7 +61,10 @@ def build(self, messages: list[Message], tools: list[ToolDefinition] | None = No
system_parts.append(f"\n\n## Available Tools\n{tools_desc}")

if messages[0].role == Role.SYSTEM:
system_parts.insert(0, messages[0].content)
system_content = messages[0].content
if not isinstance(system_content, str):
raise TypeError("PromptBuilder system messages must contain text")
system_parts.insert(0, system_content)
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
result.extend(messages[1:])
else:
Expand Down
2 changes: 2 additions & 0 deletions src/llm/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider
from llm.providers.atlas import AtlasProvider
from llm.providers.claude import ClaudeProvider
from llm.providers.minimax import MiniMaxProvider
from llm.providers.openai import OpenAIProvider
from llm.providers.ollama import OllamaProvider
from llm.providers.resolver import get_provider, register_provider
Expand All @@ -12,6 +13,7 @@
"AstraflowProvider",
"AtlasProvider",
"ClaudeProvider",
"MiniMaxProvider",
"OpenAIProvider",
"OllamaProvider",
"get_provider",
Expand Down
8 changes: 7 additions & 1 deletion src/llm/providers/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ def __init__(self, api_key: str | None = None, base_url: str | None = None) -> N
def generate(self, input: LLMInput) -> LLMOutput:
try:
model = input.model or _DEFAULT_MODEL
system_parts = [msg.content for msg in input.messages if msg.role == Role.SYSTEM]
system_parts: list[str] = []
for message in input.messages:
if message.role != Role.SYSTEM:
continue
if not isinstance(message.content, str):
raise TypeError("System messages must contain text")
system_parts.append(message.content)
api_messages = [
msg.to_dict() for msg in input.messages if msg.role not in (Role.SYSTEM,)
]
Expand Down
Loading