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
2 changes: 2 additions & 0 deletions .github/workflows/ubuntu-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ jobs:
- name: Run tests
run: |
pytest
env:
CECLI_TUI: "false"
2 changes: 2 additions & 0 deletions .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ jobs:
- name: Run tests
run: |
pytest
env:
CECLI_TUI: "false"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ LLMs are a part of our lives from here on out so join us in learning about and c
* [Skills](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/skills.md)
* [Session Management](https://github.com/dwash96/cecli/blob/main/aider/website/docs/sessions.md)
* [Custom Commands](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-commands.md)
* [Custom System Prompts](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-system-prompts.md)
* [Custom Tools](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/agent-mode.md#creating-custom-tools)
* [Advanced Model Configuration](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/model-aliases.md#advanced-model-settings)
* [Aider Original Documentation (still mostly applies)](https://aider.chat/)
Expand Down
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.93.0.dev"
__version__ = "0.95.7.dev"
safe_version = __version__

try:
Expand Down
35 changes: 25 additions & 10 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,34 @@ def get_parser(default_config_files, git_root):
),
)

#######
group = parser.add_argument_group("Customization Settings")
group.add_argument(
"--custom",
metavar="CUSTOM_JSON",
help=(
"Specify cecli customizations configurations (for prompts, commands, etc.) as a JSON"
" string"
),
default=None,
)
########
group = parser.add_argument_group("TUI Settings")

env_val = os.environ.get("CECLI_TUI")

if env_val is not None and env_val.lower() == "false":
tui_default = False
linear_output_default = True
else:
tui_default = None
linear_output_default = None

group.add_argument(
"--tui",
action=argparse.BooleanOptionalAction,
default=None,
help="Launch Textual TUI interface (experimental)",
default=tui_default,
help="Launch Textual TUI interface",
)
group.add_argument(
"--tui-config",
Expand Down Expand Up @@ -803,8 +824,8 @@ def get_parser(default_config_files, git_root):
group.add_argument(
"--linear-output",
action=argparse.BooleanOptionalAction,
help="Run input and output sequentially instead of us simultaneous streams (default: True)",
default=True,
help="Run input and output sequentially instead of us simultaneous streams (default: None)",
default=linear_output_default,
)
group.add_argument(
"--debug",
Expand Down Expand Up @@ -975,12 +996,6 @@ def get_parser(default_config_files, git_root):
" specified, a default command for your OS may be used."
),
)
group.add_argument(
"--command-paths",
help="JSON array of paths to custom commands files",
action="append",
default=None,
)
group.add_argument(
"--command-prefix",
default=None,
Expand Down
88 changes: 50 additions & 38 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from cecli import urls, utils
from cecli.change_tracker import ChangeTracker
from cecli.helpers import nested
from cecli.helpers.similarity import (
cosine_similarity,
create_bigram_vector,
Expand Down Expand Up @@ -105,50 +106,61 @@ def _get_agent_config(self):
self.io.tool_warning(f"Failed to parse agent-config JSON: {e}")
return {}

if "large_file_token_threshold" not in config:
config["large_file_token_threshold"] = 25000

if "tools_paths" not in config:
config["tools_paths"] = []
if "tools_includelist" not in config:
config["tools_includelist"] = []
if "tools_excludelist" not in config:
config["tools_excludelist"] = []
config["large_file_token_threshold"] = nested.getter(
config, "large_file_token_threshold", 25000
)
config["skip_cli_confirmations"] = nested.getter(
config, "skip_cli_confirmations", nested.getter(config, "yolo", [])
)

if "include_context_blocks" in config:
self.allowed_context_blocks = set(config["include_context_blocks"])
else:
self.allowed_context_blocks = {
"context_summary",
"directory_structure",
"environment_info",
"git_status",
"symbol_outline",
"todo_list",
"skills",
}
config["tools_paths"] = nested.getter(config, "tools_paths", [])
config["tools_includelist"] = nested.getter(
config, ["tools_includelist", "tools_whitelist"], []
)
config["tools_excludelist"] = nested.getter(
config, ["tools_excludelist", "tools_blacklist"], []
)

if "exclude_context_blocks" in config:
for context_block in config["exclude_context_blocks"]:
try:
self.allowed_context_blocks.remove(context_block)
except KeyError:
pass
config["include_context_blocks"] = set(
nested.getter(
config,
"include_context_blocks",
{
"context_summary",
"directory_structure",
"environment_info",
"git_status",
"symbol_outline",
"todo_list",
"skills",
},
)
)
config["exclude_context_blocks"] = set(nested.getter(config, "exclude_context_blocks", []))

self.large_file_token_threshold = config["large_file_token_threshold"]
self.skip_cli_confirmations = config.get(
"skip_cli_confirmations", config.get("yolo", False)
)
self.skip_cli_confirmations = config["skip_cli_confirmations"]

self.allowed_context_blocks = config["include_context_blocks"]

for context_block in config["exclude_context_blocks"]:
try:
self.allowed_context_blocks.remove(context_block)
except KeyError:
pass

if "skills" in self.allowed_context_blocks:
if "skills_paths" not in config:
config["skills_paths"] = []
if "skills_includelist" not in config:
config["skills_includelist"] = []
if "skills_excludelist" not in config:
config["skills_excludelist"] = []

if "skills" not in self.allowed_context_blocks or not config.get("skills_paths", []):
config["skills_paths"] = nested.getter(config, "skills_paths", [])
config["skills_includelist"] = nested.getter(
config, ["skills_includelist", "skills_whitelist"], []
)
config["skills_excludelist"] = nested.getter(
config, ["skills_excludelist", "skills_blacklist"], []
)

if "skills" not in self.allowed_context_blocks or not nested.getter(
config, "skills_paths", []
):
config["tools_excludelist"].append("loadskill")
config["tools_excludelist"].append("removeskill")

Expand Down
36 changes: 26 additions & 10 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from cecli import __version__, models, urls, utils
from cecli.commands import Commands, SwitchCoderSignal
from cecli.exceptions import LiteLLMExceptions
from cecli.helpers import coroutines
from cecli.helpers import coroutines, nested
from cecli.helpers.profiler import TokenProfiler
from cecli.history import ChatSummary
from cecli.io import ConfirmGroup, InputOutput
Expand All @@ -61,7 +61,7 @@
from cecli.utils import format_tokens, is_image_file

from ..dump import dump # noqa: F401
from ..prompts.utils.prompt_registry import registry
from ..prompts.utils.registry import PromptObject, PromptRegistry
from .chat_chunks import ChatChunks


Expand Down Expand Up @@ -564,6 +564,29 @@ def __init__(
except Exception as e:
self.io.tool_warning(f"Could not remove todo list file {todo_file_path}: {e}")

customizations = dict()
try:
if self.args:
customizations = nested.getter(self.args, "custom", "{}")
customizations = json.loads(customizations)
except (json.JSONDecodeError, TypeError):
customizations = dict()
pass

self.custom = customizations

if nested.getter(self.custom, "prompt_map.all", None):
prompts = PromptRegistry.get_prompt(nested.getter(self.custom, "prompt_map.all"))
prompt_obj = PromptObject(prompts)
Coder._prompt_cache[self.prompt_format] = prompt_obj

if nested.getter(self.custom, f"prompt_map.{self.prompt_format}", None):
prompts = PromptRegistry.get_prompt(
nested.getter(self.custom, f"prompt_map.{self.prompt_format}")
)
prompt_obj = PromptObject(prompts)
Coder._prompt_cache[self.prompt_format] = prompt_obj

# validate the functions jsonschema
if self.functions:
from jsonschema import Draft7Validator
Expand Down Expand Up @@ -600,14 +623,7 @@ def gpt_prompts(self):
return Coder._prompt_cache[prompt_name]

# Get prompts from registry
prompts = registry.get_prompt(prompt_name)

# Create a simple object that allows attribute access
class PromptObject:
def __init__(self, prompts_dict):
for key, value in prompts_dict.items():
setattr(self, key, value)

prompts = PromptRegistry.get_prompt(prompt_name)
# Cache the prompt object
prompt_obj = PromptObject(prompts)
Coder._prompt_cache[prompt_name] = prompt_obj
Expand Down
17 changes: 10 additions & 7 deletions cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path

from cecli.commands.utils.registry import CommandRegistry
from cecli.helpers import plugin_manager
from cecli.helpers import nested, plugin_manager
from cecli.helpers.file_searcher import handle_core_files
from cecli.repo import ANY_GIT_ERROR

Expand Down Expand Up @@ -80,13 +80,16 @@ def __init__(
self.editor = editor
self.original_read_only_fnames = set(original_read_only_fnames or [])

customizations = dict()
try:
self.custom_commands = json.loads(getattr(self.args, "command_paths", "[]"))
except (json.JSONDecodeError, TypeError) as e:
self.io.tool_warning(f"Failed to parse command paths JSON: {e}")
self.custom_commands = []

# Load custom commands from plugin paths
if self.args:
customizations = nested.getter(self.args, "custom", "{}")
customizations = json.loads(customizations)
except (json.JSONDecodeError, TypeError):
customizations = dict()
pass

self.custom_commands = nested.getter(customizations, "command-paths", [])
self._load_custom_commands(self.custom_commands)

self.cmd_running_event = asyncio.Event()
Expand Down
53 changes: 51 additions & 2 deletions cecli/helpers/model_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,40 @@ def _get_cache_file(self, provider: str) -> Path:
fname = f"{provider}_models.json"
return self.cache_dir / fname

def _normalize_models_payload(self, provider: str, payload: Dict) -> Dict:
"""Normalize provider payloads into an OpenAI-style `{data: [{id: ...}]}`."""
if not isinstance(payload, dict):
return {}
if "data" in payload and isinstance(payload.get("data"), list):
return payload
# Fireworks returns `{models: [...], nextPageToken: ..., totalSize: ...}`
models = payload.get("models")
if isinstance(models, list):
normalized = []
for item in models:
if not isinstance(item, dict):
continue
model_id = item.get("name") or item.get("id")
if not model_id:
continue
record = {"id": model_id}
for key in (
"max_input_tokens",
"max_output_tokens",
"max_tokens",
"context_length",
"context_window",
"mode",
"pricing",
"input_cost_per_token",
"output_cost_per_token",
):
if key in item and item[key] is not None:
record[key] = item[key]
normalized.append(record)
return {"data": normalized}
return {}

def _load_cache(self, provider: str) -> None:
if self._cache_loaded.get(provider):
return
Expand All @@ -460,9 +494,10 @@ def _update_cache(self, provider: str) -> None:
payload = self._fetch_provider_models(provider)
cache_file = self._get_cache_file(provider)
if payload:
self._provider_cache[provider] = payload
normalized = self._normalize_models_payload(provider, payload)
self._provider_cache[provider] = normalized
try:
cache_file.write_text(json.dumps(payload, indent=2))
cache_file.write_text(json.dumps(normalized, indent=2))
except OSError:
pass
return
Expand All @@ -479,6 +514,13 @@ def _fetch_provider_models(self, provider: str) -> Optional[Dict]:
models_url = api_base.rstrip("/") + "/models"
if not models_url:
return None
# Substitute {account_id} placeholder if present
if "{account_id}" in models_url:
account_id = self._get_account_id(provider)
if not account_id:
print(f"Failed to fetch {provider} model list: account_id_env not set")
return None
models_url = models_url.replace("{account_id}", account_id)
headers = {}
default_headers = config.get("default_headers") or {}
headers.update(default_headers)
Expand Down Expand Up @@ -509,6 +551,13 @@ def _get_api_key(self, provider: str) -> Optional[str]:
return value
return None

def _get_account_id(self, provider: str) -> Optional[str]:
config = self.provider_configs[provider]
account_id_env = config.get("account_id_env")
if account_id_env:
return os.environ.get(account_id_env)
return None


def ensure_litellm_providers_registered() -> None:
"""One-time registration guard for LiteLLM provider metadata."""
Expand Down
Loading
Loading