Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3334c8a
add an example docker setup for local nvidia
Dec 27, 2025
ea0893c
change local docker from aider to aider-ce
Dec 27, 2025
33f1be8
Add OpenAI-compatible provider registry
chrisnestrud Dec 28, 2025
5fb0c20
Normalize provider pricing strings
chrisnestrud Dec 28, 2025
b3a8ae9
Ensure non-streaming answers survive reasoning blocks
chrisnestrud Dec 28, 2025
01f530b
Run formatting hooks after provider/test updates
chrisnestrud Dec 28, 2025
e5978a6
docs: clarify streaming warning with config key guidance
chrisnestrud Dec 28, 2025
61ca039
feat: unify model provider metadata management
chrisnestrud Dec 28, 2025
564dcdf
test: cover unified model provider manager
chrisnestrud Dec 28, 2025
ffbc8be
test: add fallback coverage for unified provider cache
chrisnestrud Dec 28, 2025
ab7e70e
test: retire legacy provider-specific suites
chrisnestrud Dec 28, 2025
a2f773f
chore: rename provider resource file
chrisnestrud Dec 28, 2025
eb511ff
chore: document helper functions in provider manager
chrisnestrud Dec 28, 2025
4164dca
docs: clarify unified provider rationale
chrisnestrud Dec 28, 2025
ce524d3
chore: document provider scripts and rename generator
chrisnestrud Dec 28, 2025
e365eaa
style: apply pre-commit formatting fixes
chrisnestrud Dec 28, 2025
802e130
Bump Version
dwash96 Dec 29, 2025
6cb84da
build: Add pytest-mock
johbo Dec 29, 2025
8256d68
build: Add pytest-mock related updates to requirement files
johbo Dec 29, 2025
0c2c6d5
test: add smoke tests to verify main entry points execute
johbo Dec 29, 2025
a015888
refactor: convert test_main async tests to synchronous
johbo Dec 29, 2025
1c37468
fix: replace ambiguous --yes flag with --yes-always in tests
johbo Dec 29, 2025
3c97313
test: Flag test_add_command_gitignore_files as xfail
johbo Dec 29, 2025
882145b
fix: add autospec=True to InputOutput mocks and configure pretty attr…
johbo Dec 29, 2025
9e6d24a
fix: remove incorrect async from create_env_file helper method
johbo Dec 29, 2025
7878678
test: fix Coder.create mock to handle _autosave_future
johbo Dec 29, 2025
5f59246
test: fix InputOutput mock in test_encodings_arg
johbo Dec 29, 2025
a366831
test: fix InputOutput mock in test_main_exit_calls_version_check
johbo Dec 29, 2025
2570174
test: mark test_lint_option as xfail
johbo Dec 29, 2025
f07020e
test: update tests to expect exit code 0 instead of None
johbo Dec 29, 2025
9b1160c
style: apply black formatting to test files
johbo Dec 29, 2025
633cdd2
ci: Install pytest-mock in test actions
johbo Dec 29, 2025
8e148a6
fix: set USERPROFILE for Windows compatibility in smoke tests
johbo Dec 29, 2025
6b76530
test: convert async tests to sync using asyncio.run()
johbo Dec 29, 2025
9682e63
test: update test_add_command_gitignore_files_flag for new command ar…
johbo Dec 29, 2025
6d0655e
fix: update LintCommand to read files from system_args and support globs
johbo Dec 29, 2025
07bf35c
refactor: remove duplicate path resolution in LintCommand
johbo Dec 29, 2025
45ee516
refactor: remove redundant coder.repo check in LintCommand
johbo Dec 29, 2025
4172b39
refactor: remove unused root parameter from expand_glob_patterns
johbo Dec 29, 2025
96ddfb9
style: fix linting issues
johbo Dec 29, 2025
32c96ea
Don't reinitialize mcp servers on coder switch if server exists
dwash96 Dec 30, 2025
ac1315c
#330: Read only completions fix
dwash96 Dec 30, 2025
126675b
Merge pull request #319 from minger0/main
dwash96 Dec 30, 2025
9e9d278
Merge pull request #327 from johbo/fix-test-main
dwash96 Dec 30, 2025
9b408df
Update dependency files and documentation
dwash96 Dec 30, 2025
859c45c
Fix overriding of copypastecoder model
dwash96 Dec 30, 2025
0c43224
Merge remote-tracking branch 'chrisnestrud/feature/openai-providers' …
dwash96 Dec 30, 2025
c6a90d5
Offset token costs by 1000000 for provider manager since most quotes …
dwash96 Dec 30, 2025
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 .github/workflows/ubuntu-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
uv pip install --system \
pytest \
pytest-asyncio \
pytest-mock \
-r requirements/requirements.in \
-r requirements/requirements-help.in \
-r requirements/requirements-playwright.in \
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install uv
uv pip install --system pytest pytest-asyncio -r requirements/requirements.in -r requirements/requirements-help.in -r requirements/requirements-playwright.in '.[help,playwright]'
uv pip install --system pytest pytest-asyncio pytest-mock -r requirements/requirements.in -r requirements/requirements-help.in -r requirements/requirements-playwright.in '.[help,playwright]'

- name: Run tests
env:
AIDER_ANALYTICS: false
run: |
pytest

2 changes: 1 addition & 1 deletion aider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.91.2.dev"
__version__ = "0.91.3.dev"
safe_version = __version__

try:
Expand Down
1 change: 1 addition & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ def get_parser(default_config_files, git_root):
)
group.add_argument(
"--yes-always",
"--yes",
action="store_true",
help="Always say yes to every confirmation (not including cli commands)",
default=None,
Expand Down
29 changes: 16 additions & 13 deletions aider/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,23 +369,26 @@ def get_local_tool_schemas(self):
async def initialize_mcp_tools(self):
await super().initialize_mcp_tools()

local_tools = self.get_local_tool_schemas()
if not local_tools:
return
server_name = "Local"

if server_name not in [name for name, _ in self.mcp_tools]:
local_tools = self.get_local_tool_schemas()
if not local_tools:
return

local_server_config = {"name": "Local"}
local_server = LocalServer(local_server_config)
local_server_config = {"name": server_name}
local_server = LocalServer(local_server_config)

if not self.mcp_servers:
self.mcp_servers = []
if not any(isinstance(s, LocalServer) for s in self.mcp_servers):
self.mcp_servers.append(local_server)
if not self.mcp_servers:
self.mcp_servers = []
if not any(isinstance(s, LocalServer) for s in self.mcp_servers):
self.mcp_servers.append(local_server)

if not self.mcp_tools:
self.mcp_tools = []
if not self.mcp_tools:
self.mcp_tools = []

if "local_tools" not in [name for name, _ in self.mcp_tools]:
self.mcp_tools.append((local_server.name, local_tools))
if server_name not in [name for name, _ in self.mcp_tools]:
self.mcp_tools.append((local_server.name, local_tools))

async def _execute_local_tool_calls(self, tool_calls_list):
tool_responses = []
Expand Down
8 changes: 0 additions & 8 deletions aider/coders/architect_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ async def reply_completed(self):
kwargs["cache_prompts"] = False
kwargs["num_cache_warming_pings"] = 0
kwargs["summarize_from_coder"] = False
kwargs["mcp_servers"] = [] # Empty to skip initialization

coder = await Coder.create(**kwargs)
# Transfer MCP state to avoid re-initialization
coder.mcp_servers = self.mcp_servers
coder.mcp_tools = self.mcp_tools
# Transfer TUI app weak reference
coder.tui = self.tui

new_kwargs = dict(io=self.io, from_coder=self)
new_kwargs.update(kwargs)
Expand Down
46 changes: 37 additions & 9 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,32 @@ async def create(
kwargs = use_kwargs
from_coder.ok_to_warm_cache = False

res = None
if (
getattr(main_model, "copy_paste_mode", False)
and getattr(main_model, "copy_paste_transport", "api") == "clipboard"
):
res = coders.CopyPasteCoder(main_model, io, args=args, **kwargs)

if not res:
for coder in coders.__all__:
if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
res = coder(main_model, io, args=args, **kwargs)

if res is not None:
if from_coder:
if from_coder.mcp_servers and kwargs.get("mcp_servers", False):
res.mcp_servers = from_coder.mcp_servers
res.mcp_tools = from_coder.mcp_tools

# Transfer TUI app weak reference
res.tui = from_coder.tui

await res.initialize_mcp_tools()

res.original_kwargs = dict(kwargs)
return res

for coder in coders.__all__:
if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
res = coder(main_model, io, args=args, **kwargs)
await res.initialize_mcp_tools()
res.original_kwargs = dict(kwargs)
return res

valid_formats = [
str(c.edit_format)
for c in coders.__all__
Expand Down Expand Up @@ -2703,6 +2713,12 @@ async def initialize_mcp_tools(self):
tools = []

async def get_server_tools(server):
# Check if we already have tools for this server in mcp_tools
if self.mcp_tools:
for server_name, server_tools in self.mcp_tools:
if server_name == server.name:
return (server.name, server_tools)

try:
session = await server.connect()
server_tools = await experimental_mcp_client.load_mcp_tools(
Expand Down Expand Up @@ -3266,8 +3282,20 @@ def consolidate_chunks(self):
self.partial_response_reasoning_content = reasoning_content or ""

try:
if not self.partial_response_reasoning_content:
self.partial_response_content = response.choices[0].message.content or ""
content = response.choices[0].message.content
if isinstance(content, list):
# OpenAI-compatible APIs sometimes return content as a list
# of blocks; join the textual pieces for display.
content = "".join(
block.get("text", "")
for block in content
if isinstance(block, dict) and block.get("type") == "output_text"
) or "".join(
block.get("text", "")
for block in content
if isinstance(block, dict) and block.get("type") == "text"
)
self.partial_response_content = content or ""
except AttributeError as e:
content_err = e

Expand Down
6 changes: 0 additions & 6 deletions aider/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,8 @@ async def execute(cls, io, coder, args, **kwargs):
kwargs["suggest_shell_commands"] = False
kwargs["cache_prompts"] = False
kwargs["num_cache_warming_pings"] = 0
kwargs["mcp_servers"] = [] # Empty to skip initialization

help_coder = await Coder.create(**kwargs)
# Transfer MCP state to avoid re-initialization
help_coder.mcp_servers = coder.mcp_servers
help_coder.mcp_tools = coder.mcp_tools
# Transfer TUI app weak reference
help_coder.tui = coder.tui
user_msg = help_instance.ask(args)
user_msg += """
# Announcement lines from when this session of aider was launched:
Expand Down
14 changes: 12 additions & 2 deletions aider/commands/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from aider.commands.utils.base_command import BaseCommand
from aider.commands.utils.helpers import format_command_result
from aider.utils import expand_glob_patterns


class LintCommand(BaseCommand):
Expand All @@ -11,7 +12,16 @@ class LintCommand(BaseCommand):
@classmethod
async def execute(cls, io, coder, args, **kwargs):
"""Execute the lint command with given parameters."""
fnames = kwargs.get("fnames", None)
fnames = None

# Get files from CLI arguments if available
system_args = kwargs.get("system_args")
if system_args:
cli_files = getattr(system_args, "files", []) or []
cli_file_arg = getattr(system_args, "file", []) or []
all_cli_files = cli_files + cli_file_arg
if all_cli_files:
fnames = expand_glob_patterns(all_cli_files)

if not coder.repo:
io.tool_error("No git repository found.")
Expand All @@ -21,7 +31,7 @@ async def execute(cls, io, coder, args, **kwargs):
fnames = coder.get_inchat_relative_files()

# If still no files, get all dirty files in the repo
if not fnames and coder.repo:
if not fnames:
fnames = coder.repo.get_dirty_files()

if not fnames:
Expand Down
5 changes: 1 addition & 4 deletions aider/commands/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ async def execute(cls, io, coder, args, **kwargs):
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for model command."""
from aider.llm import litellm

model_names = litellm.model_cost.keys()
return list(model_names)
return models.get_chat_model_names()

@classmethod
def get_help(cls) -> str:
Expand Down
5 changes: 1 addition & 4 deletions aider/commands/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ async def execute(cls, io, coder, args, **kwargs):
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for models command."""
from aider.llm import litellm

model_names = litellm.model_cost.keys()
return list(model_names)
return models.get_chat_model_names()

@classmethod
def get_help(cls) -> str:
Expand Down
40 changes: 37 additions & 3 deletions aider/commands/read_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,43 @@ def _add_read_only_directory(
@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for read-only command."""
# For read-only command, we could return file paths for completion
# For now, return empty list - the completion system will handle path completion
return []
from pathlib import Path

root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()

# Handle the prefix - could be partial path like "src/ma" or just "ma"
if "/" in args:
# Has directory component
dir_part, file_part = args.rsplit("/", 1)
search_dir = root / dir_part
search_prefix = file_part.lower()
path_prefix = dir_part + "/"
else:
search_dir = root
search_prefix = args.lower()
path_prefix = ""

completions = []
try:
if search_dir.exists() and search_dir.is_dir():
for entry in search_dir.iterdir():
name = entry.name
if search_prefix and search_prefix not in name.lower():
continue
# Add trailing slash for directories
if entry.is_dir():
completions.append(path_prefix + name + "/")
else:
completions.append(path_prefix + name)
except (PermissionError, OSError):
pass

add_completions = coder.commands.get_completions("/add")
for c in add_completions:
if args.lower() in str(c).lower() and str(c) not in completions:
completions.append(str(c))

return sorted(completions)

@classmethod
def get_help(cls) -> str:
Expand Down
42 changes: 38 additions & 4 deletions aider/commands/read_only_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,44 @@ def _add_read_only_directory(

@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for read-only-stub command."""
# For read-only-stub command, we could return file paths for completion
# For now, return empty list - the completion system will handle path completion
return []
"""Get completion options for read-only command."""
from pathlib import Path

root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()

# Handle the prefix - could be partial path like "src/ma" or just "ma"
if "/" in args:
# Has directory component
dir_part, file_part = args.rsplit("/", 1)
search_dir = root / dir_part
search_prefix = file_part.lower()
path_prefix = dir_part + "/"
else:
search_dir = root
search_prefix = args.lower()
path_prefix = ""

completions = []
try:
if search_dir.exists() and search_dir.is_dir():
for entry in search_dir.iterdir():
name = entry.name
if search_prefix and search_prefix not in name.lower():
continue
# Add trailing slash for directories
if entry.is_dir():
completions.append(path_prefix + name + "/")
else:
completions.append(path_prefix + name)
except (PermissionError, OSError):
pass

add_completions = coder.commands.get_completions("/add")
for c in add_completions:
if args.lower() in str(c).lower() and str(c) not in completions:
completions.append(str(c))

return sorted(completions)

@classmethod
def get_help(cls) -> str:
Expand Down
7 changes: 0 additions & 7 deletions aider/commands/utils/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,7 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N
"args": coder.args,
}

kwargs["mcp_servers"] = [] # Empty to skip initialization

new_coder = await Coder.create(**kwargs)
# Transfer MCP state to avoid re-initialization
new_coder.mcp_servers = coder.mcp_servers
new_coder.mcp_tools = coder.mcp_tools
# Transfer TUI app weak reference
new_coder.tui = coder.tui

await new_coder.generate(user_message=user_msg, preproc=False)
coder.aider_commit_hashes = new_coder.aider_commit_hashes
Expand Down
Loading
Loading