Skip to content
Merged
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 pantheon/factory/templates/prompts/delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,4 @@ call_agent(
```python
call_agent("researcher", "Do analysis fast.")
```

10 changes: 7 additions & 3 deletions pantheon/factory/templates/teams/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ call_agent("researcher", "Search the web for best practices on X. Gather informa
- Data analysis, EDA, statistical analysis
- Literature review and multi-source research

**Scientific writing gate (MANDATORY):** Before writing any report, paper, or document that requires domain knowledge or citations, you MUST first delegate a research task to `researcher`. Writing without a prior research delegation is not allowed for these task types.

#### Scientific Illustrator

**Delegate for:** Scientific diagrams, publication-quality visualizations, complex figures
**Execute directly:** Simple chart embedding, displaying existing charts
**Delegate for:** Schematic diagrams, conceptual illustrations, architecture diagrams, publication-quality figures — tasks where the output is a conceptual diagram, not a data-driven chart.
**Execute directly (or via Researcher):** Data visualizations, statistical plots, charts derived from analysis results.

### Decision Summary

Expand All @@ -100,9 +102,11 @@ call_agent("researcher", "Search the web for best practices on X. Gather informa
| Explore/read/understand codebase | **MUST delegate** to researcher |
| Web search or documentation lookup | **MUST delegate** to researcher |
| Data analysis or research | **MUST delegate** to researcher |
| Scientific writing (report/paper) | **MUST delegate research first**, then write |
| Multiple independent research tasks | **MUST parallelize** with multiple researchers |
| Schematic/pathway/cell diagrams | **Delegate** to scientific_illustrator |
| Read 1 known file | Execute directly |
| Write/edit/create files | Execute directly |
| Write/edit/create files (post-research) | Execute directly |
| Synthesize researcher results | Execute directly (your core role) |

{{delegation}}
42 changes: 29 additions & 13 deletions pantheon/toolsets/file/file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,35 +816,51 @@ async def write_file(
file_path: str,
content: str = "",
overwrite: bool = True,
append: bool = False,
) -> dict:
"""Use this tool to CREATE NEW file.
"""Create a new file, overwrite an existing one, or append to it.

This tool writes content to a file, automatically creating parent
directories if they do not exist.
Parent directories are created automatically if they do not exist.

IMPORTANT: For EDITING existing file, use `update_file` instead.
DO NOT rewrite entire file when only small changes are needed, its is wasteful and error-prone.
For EDITING existing files, prefer `update_file` instead — it is
safer and more efficient for partial modifications.

Use this tool when:
- Creating a brand new file
- Completely rewriting a file from scratch (rare)
- Completely rewriting a file from scratch
- Appending content to an existing file (set append=True)

DO NOT use this tool when:
- Making partial modifications to an existing file
- Changing a few lines in a large file
- For these cases, use `update_file` instead
Do NOT use this tool when:
- Making partial modifications to an existing file (use `update_file`)
- Changing a few lines in a large file (use `update_file`)

Args:
file_path: The path to the file to write.
content: The content to write to the file.
overwrite: When False, abort if the target file already exists.
Default is True, but consider using update_file for edits.
overwrite: When False, abort if the target file already exists (ignored when append=True).
append: When True, append content to the end of an existing file instead of overwriting.
The file must already exist when using append mode.

Returns:
dict: Success status or error message.
"""

target_path = self._resolve_path(file_path)

if append:
if not target_path.exists():
return {
"success": False,
"error": f"File '{file_path}' does not exist. Use write_file without append=True to create it first.",
"reason": "file_not_found",
}
try:
with open(target_path, "a", encoding="utf-8") as f:
f.write(content)
return {"success": True, "appended_chars": len(content)}
except Exception as exc:
logger.error(f"write_file(append) failed for {file_path}: {exc}")
return {"success": False, "error": str(exc)}

if not overwrite and target_path.exists():
return {
"success": False,
Expand Down
1 change: 0 additions & 1 deletion pantheon/utils/adapters/openai_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def _normalize_response_format(response_format: Any) -> Any:
pass
return response_format


class OpenAIAdapter(BaseAdapter):
"""Adapter for OpenAI and OpenAI-compatible APIs."""

Expand Down
40 changes: 38 additions & 2 deletions pantheon/utils/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ async def acompletion_responses(
"""
from openai import AsyncOpenAI
from .llm_providers import get_proxy_kwargs
from .provider_registry import get_model_info, get_output_token_param

# ========== Build client ==========
proxy_kwargs = get_proxy_kwargs()
Expand All @@ -257,7 +258,19 @@ async def acompletion_responses(
# ========== Convert inputs ==========
instructions, input_items = _convert_messages_to_responses_input(messages)
converted_tools = _convert_tools_for_responses(tools)
extra_params = _convert_model_params_for_responses(model_params)
response_model_params = dict(model_params or {})
if not any(
key in response_model_params
for key in ("max_tokens", "max_completion_tokens", "max_output_tokens")
):
try:
max_out = get_model_info(model).get("max_output_tokens")
token_param = get_output_token_param(model, api_mode="responses")
if token_param and max_out and max_out > 0:
response_model_params[token_param] = max_out
except Exception:
pass
extra_params = _convert_model_params_for_responses(response_model_params)

# ========== Build kwargs ==========
kwargs: dict[str, Any] = {
Expand Down Expand Up @@ -553,7 +566,13 @@ async def acompletion(
- Uses native SDK adapters (openai, anthropic, google-genai)
"""
from .llm_providers import get_proxy_kwargs
from .provider_registry import find_provider_for_model, get_provider_config, completion_cost
from .provider_registry import (
find_provider_for_model,
get_provider_config,
completion_cost,
get_model_info,
get_output_token_param,
)
from .adapters import get_adapter

logger.debug(f"[ACOMPLETION] Starting LLM call | Model={model}")
Expand All @@ -562,6 +581,23 @@ async def acompletion(
provider_key, model_name, provider_config = find_provider_for_model(model)
sdk_type = provider_config.get("sdk", "openai")

# ========== Ensure output token limit is set from the catalog ==========
# Different vendors use different parameter names for the same concept.
# The catalog records the preferred parameter name; we use it here so the
# first request is correct for known providers/models.
model_params = dict(model_params or {})
if not any(
key in model_params
for key in ("max_tokens", "max_completion_tokens", "max_output_tokens")
):
try:
max_out = get_model_info(model).get("max_output_tokens")
token_param = get_output_token_param(model, api_mode="chat")
if token_param and max_out and max_out > 0:
model_params[token_param] = max_out
except Exception:
pass # Fall through to provider default

# ========== Mode Detection & Configuration ==========
proxy_kwargs = get_proxy_kwargs()
if proxy_kwargs:
Expand Down
31 changes: 31 additions & 0 deletions pantheon/utils/llm_catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"base_url": "https://api.openai.com/v1",
"api_key_env": "OPENAI_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_completion_tokens",
"responses_output_token_param": "max_output_tokens",
"models": {
"gpt-5.4-pro": {
"max_input_tokens": 1000000,
Expand Down Expand Up @@ -184,6 +186,22 @@
"supports_computer_use": false,
"supports_assistant_prefill": false
},
"gpt-4o-mini": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6,
"supports_vision": true,
"supports_function_calling": true,
"supports_response_schema": true,
"supports_reasoning": false,
"supports_audio_input": false,
"supports_audio_output": false,
"supports_web_search": false,
"supports_pdf_input": false,
"supports_computer_use": false,
"supports_assistant_prefill": false
},
"o3-pro": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
Expand Down Expand Up @@ -307,6 +325,7 @@
"base_url": "https://api.anthropic.com",
"api_key_env": "ANTHROPIC_API_KEY",
"openai_compatible": false,
"chat_output_token_param": "max_tokens",
"models": {
"claude-opus-4-6": {
"max_input_tokens": 1000000,
Expand Down Expand Up @@ -428,6 +447,7 @@
"base_url": "https://generativelanguage.googleapis.com",
"api_key_env": "GEMINI_API_KEY",
"openai_compatible": false,
"chat_output_token_param": "max_output_tokens",
"models": {
"gemini-3.1-pro-preview": {
"max_input_tokens": 2000000,
Expand Down Expand Up @@ -560,6 +580,7 @@
"base_url": "https://api.deepseek.com/v1",
"api_key_env": "DEEPSEEK_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"deepseek-chat": {
"max_input_tokens": 131072,
Expand Down Expand Up @@ -601,6 +622,7 @@
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_key_env": "ZAI_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"glm-5": {
"max_input_tokens": 131072,
Expand Down Expand Up @@ -706,6 +728,7 @@
"base_url": "https://api.minimax.io/v1",
"api_key_env": "MINIMAX_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"MiniMax-M2.7": {
"max_input_tokens": 1000000,
Expand Down Expand Up @@ -795,6 +818,7 @@
"base_url": "https://api.moonshot.ai/v1",
"api_key_env": "MOONSHOT_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"kimi-k2.5": {
"max_input_tokens": 131072,
Expand Down Expand Up @@ -836,6 +860,7 @@
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key_env": "DASHSCOPE_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"qwen3-235b-a22b": {
"max_input_tokens": 131072,
Expand Down Expand Up @@ -989,6 +1014,7 @@
"base_url": "https://api.groq.com/openai/v1",
"api_key_env": "GROQ_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_completion_tokens",
"models": {
"openai/gpt-oss-120b": {
"max_input_tokens": 131072,
Expand Down Expand Up @@ -1110,6 +1136,7 @@
"base_url": "https://openrouter.ai/api/v1",
"api_key_env": "OPENROUTER_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"anthropic/claude-sonnet-4-6": {
"max_input_tokens": 1000000,
Expand Down Expand Up @@ -1183,6 +1210,7 @@
"base_url": "https://api.mistral.ai/v1",
"api_key_env": "MISTRAL_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"mistral-large-latest": {
"max_input_tokens": 262144,
Expand Down Expand Up @@ -1272,6 +1300,7 @@
"base_url": "https://api.together.xyz/v1",
"api_key_env": "TOGETHER_API_KEY",
"openai_compatible": true,
"chat_output_token_param": "max_tokens",
"models": {
"Qwen/Qwen3.5-397B-A17B": {
"max_input_tokens": 262144,
Expand Down Expand Up @@ -1346,6 +1375,7 @@
"api_key_env": "",
"openai_compatible": false,
"auth_mode": "oauth",
"responses_output_token_param": "max_output_tokens",
"models": {
"gpt-5.4": {
"max_input_tokens": 1000000,
Expand Down Expand Up @@ -1406,6 +1436,7 @@
"api_key_env": "",
"openai_compatible": true,
"local": true,
"chat_output_token_param": "max_tokens",
"models": {}
}
}
Expand Down
16 changes: 15 additions & 1 deletion pantheon/utils/provider_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"supports_assistant_prefill": False,
}


@lru_cache(maxsize=1)
def load_catalog() -> dict:
"""Load and cache the provider catalog from llm_catalog.json."""
Expand Down Expand Up @@ -98,6 +97,21 @@ def get_provider_config(provider: str) -> dict:
return catalog.get("providers", {}).get(provider, {})


def get_output_token_param(model: str, api_mode: str = "chat") -> str | None:
"""Return the provider/model-specific output token parameter name.

Args:
model: Model string, e.g. ``openai/gpt-5.4`` or ``gpt-4o-mini``.
api_mode: ``chat`` for chat/completions style APIs, ``responses`` for
OpenAI Responses-style APIs.
"""
_provider_key, _model_name, provider_config = find_provider_for_model(model)
if api_mode == "responses":
return provider_config.get("responses_output_token_param")

return provider_config.get("chat_output_token_param")


# ============ Model Metadata ============


Expand Down
Loading
Loading