aider-ce 0.87.13__py3-none-any.whl → 0.88.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aider-ce might be problematic. Click here for more details.
- aider/__init__.py +1 -1
- aider/_version.py +2 -2
- aider/args.py +6 -0
- aider/coders/architect_coder.py +3 -3
- aider/coders/base_coder.py +505 -184
- aider/coders/context_coder.py +1 -1
- aider/coders/editblock_func_coder.py +2 -2
- aider/coders/navigator_coder.py +451 -649
- aider/coders/navigator_legacy_prompts.py +49 -284
- aider/coders/navigator_prompts.py +46 -473
- aider/coders/search_replace.py +0 -0
- aider/coders/wholefile_func_coder.py +2 -2
- aider/commands.py +56 -44
- aider/history.py +14 -12
- aider/io.py +354 -117
- aider/llm.py +12 -4
- aider/main.py +22 -19
- aider/mcp/__init__.py +65 -2
- aider/mcp/server.py +37 -11
- aider/models.py +45 -20
- aider/onboarding.py +4 -4
- aider/repo.py +7 -7
- aider/resources/model-metadata.json +8 -8
- aider/scrape.py +2 -2
- aider/sendchat.py +185 -15
- aider/tools/__init__.py +44 -23
- aider/tools/command.py +18 -0
- aider/tools/command_interactive.py +18 -0
- aider/tools/delete_block.py +23 -0
- aider/tools/delete_line.py +19 -1
- aider/tools/delete_lines.py +20 -1
- aider/tools/extract_lines.py +25 -2
- aider/tools/git.py +142 -0
- aider/tools/grep.py +47 -2
- aider/tools/indent_lines.py +25 -0
- aider/tools/insert_block.py +26 -0
- aider/tools/list_changes.py +15 -0
- aider/tools/ls.py +24 -1
- aider/tools/make_editable.py +18 -0
- aider/tools/make_readonly.py +19 -0
- aider/tools/remove.py +22 -0
- aider/tools/replace_all.py +21 -0
- aider/tools/replace_line.py +20 -1
- aider/tools/replace_lines.py +21 -1
- aider/tools/replace_text.py +22 -0
- aider/tools/show_numbered_context.py +18 -0
- aider/tools/undo_change.py +15 -0
- aider/tools/update_todo_list.py +131 -0
- aider/tools/view.py +23 -0
- aider/tools/view_files_at_glob.py +32 -27
- aider/tools/view_files_matching.py +51 -37
- aider/tools/view_files_with_symbol.py +41 -54
- aider/tools/view_todo_list.py +57 -0
- aider/waiting.py +20 -203
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
aider/llm.py
CHANGED
|
@@ -20,8 +20,20 @@ VERBOSE = False
|
|
|
20
20
|
|
|
21
21
|
class LazyLiteLLM:
|
|
22
22
|
_lazy_module = None
|
|
23
|
+
_lazy_classes = {
|
|
24
|
+
"ModelResponse": "ModelResponse",
|
|
25
|
+
"Choices": "Choices",
|
|
26
|
+
"Message": "Message",
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
def __getattr__(self, name):
|
|
30
|
+
# Check if the requested attribute is one of the explicitly lazy-loaded classes
|
|
31
|
+
if name in self._lazy_classes:
|
|
32
|
+
self._load_litellm()
|
|
33
|
+
class_name = self._lazy_classes[name]
|
|
34
|
+
return getattr(self._lazy_module, class_name)
|
|
35
|
+
|
|
36
|
+
# Handle other attributes (like `acompletion`) as before
|
|
25
37
|
if name == "_lazy_module":
|
|
26
38
|
return super()
|
|
27
39
|
self._load_litellm()
|
|
@@ -31,11 +43,7 @@ class LazyLiteLLM:
|
|
|
31
43
|
if self._lazy_module is not None:
|
|
32
44
|
return
|
|
33
45
|
|
|
34
|
-
if VERBOSE:
|
|
35
|
-
print("Loading litellm...")
|
|
36
|
-
|
|
37
46
|
self._lazy_module = importlib.import_module("litellm")
|
|
38
|
-
|
|
39
47
|
self._lazy_module.suppress_debug_info = True
|
|
40
48
|
self._lazy_module.set_verbose = False
|
|
41
49
|
self._lazy_module.drop_params = True
|
aider/main.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import glob
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
@@ -470,6 +471,10 @@ def expand_glob_patterns(patterns, root="."):
|
|
|
470
471
|
|
|
471
472
|
|
|
472
473
|
def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
|
|
474
|
+
return asyncio.run(main_async(argv, input, output, force_git_root, return_coder))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def main_async(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
|
|
473
478
|
report_uncaught_exceptions()
|
|
474
479
|
|
|
475
480
|
if argv is None:
|
|
@@ -744,7 +749,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
744
749
|
right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname)
|
|
745
750
|
if right_repo_root:
|
|
746
751
|
analytics.event("exit", reason="Recursing with correct repo")
|
|
747
|
-
return
|
|
752
|
+
return await main_async(argv, input, output, right_repo_root, return_coder=return_coder)
|
|
748
753
|
|
|
749
754
|
if args.just_check_update:
|
|
750
755
|
update_available = check_version(io, just_check=True, verbose=args.verbose)
|
|
@@ -801,7 +806,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
801
806
|
alias, model = parts
|
|
802
807
|
models.MODEL_ALIASES[alias.strip()] = model.strip()
|
|
803
808
|
|
|
804
|
-
selected_model_name = select_default_model(args, io, analytics)
|
|
809
|
+
selected_model_name = await select_default_model(args, io, analytics)
|
|
805
810
|
if not selected_model_name:
|
|
806
811
|
# Error message and analytics event are handled within select_default_model
|
|
807
812
|
# It might have already offered OAuth if no model/keys were found.
|
|
@@ -816,7 +821,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
816
821
|
" found."
|
|
817
822
|
)
|
|
818
823
|
# Attempt OAuth flow because the specific model needs it
|
|
819
|
-
if offer_openrouter_oauth(io, analytics):
|
|
824
|
+
if await offer_openrouter_oauth(io, analytics):
|
|
820
825
|
# OAuth succeeded, the key should now be in os.environ.
|
|
821
826
|
# Check if the key is now present after the flow.
|
|
822
827
|
if os.environ.get("OPENROUTER_API_KEY"):
|
|
@@ -1010,7 +1015,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1010
1015
|
if not mcp_servers:
|
|
1011
1016
|
mcp_servers = []
|
|
1012
1017
|
|
|
1013
|
-
coder = Coder.create(
|
|
1018
|
+
coder = await Coder.create(
|
|
1014
1019
|
main_model=main_model,
|
|
1015
1020
|
edit_format=args.edit_format,
|
|
1016
1021
|
io=io,
|
|
@@ -1086,8 +1091,6 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1086
1091
|
analytics.event("copy-paste mode")
|
|
1087
1092
|
ClipboardWatcher(coder.io, verbose=args.verbose)
|
|
1088
1093
|
|
|
1089
|
-
coder.show_announcements()
|
|
1090
|
-
|
|
1091
1094
|
if args.show_prompts:
|
|
1092
1095
|
coder.cur_messages += [
|
|
1093
1096
|
dict(role="user", content="Hello!"),
|
|
@@ -1098,22 +1101,22 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1098
1101
|
return
|
|
1099
1102
|
|
|
1100
1103
|
if args.lint:
|
|
1101
|
-
coder.commands.cmd_lint(fnames=fnames)
|
|
1104
|
+
await coder.commands.cmd_lint(fnames=fnames)
|
|
1102
1105
|
|
|
1103
1106
|
if args.test:
|
|
1104
1107
|
if not args.test_cmd:
|
|
1105
1108
|
io.tool_error("No --test-cmd provided.")
|
|
1106
1109
|
analytics.event("exit", reason="No test command provided")
|
|
1107
1110
|
return 1
|
|
1108
|
-
coder.commands.cmd_test(args.test_cmd)
|
|
1111
|
+
await coder.commands.cmd_test(args.test_cmd)
|
|
1109
1112
|
if io.placeholder:
|
|
1110
|
-
coder.run(io.placeholder)
|
|
1113
|
+
await coder.run(io.placeholder)
|
|
1111
1114
|
|
|
1112
1115
|
if args.commit:
|
|
1113
1116
|
if args.dry_run:
|
|
1114
1117
|
io.tool_output("Dry run enabled, skipping commit.")
|
|
1115
1118
|
else:
|
|
1116
|
-
coder.commands.cmd_commit()
|
|
1119
|
+
await coder.commands.cmd_commit()
|
|
1117
1120
|
|
|
1118
1121
|
if args.lint or args.test or args.commit:
|
|
1119
1122
|
analytics.event("exit", reason="Completed lint/test/commit")
|
|
@@ -1135,7 +1138,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1135
1138
|
# For testing #2879
|
|
1136
1139
|
# from aider.coders.base_coder import all_fences
|
|
1137
1140
|
# coder.fence = all_fences[1]
|
|
1138
|
-
coder.apply_updates()
|
|
1141
|
+
await coder.apply_updates()
|
|
1139
1142
|
analytics.event("exit", reason="Applied updates")
|
|
1140
1143
|
return
|
|
1141
1144
|
|
|
@@ -1168,14 +1171,14 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1168
1171
|
io.tool_warning("Cost estimates may be inaccurate when using streaming and caching.")
|
|
1169
1172
|
|
|
1170
1173
|
if args.load:
|
|
1171
|
-
commands.cmd_load(args.load)
|
|
1174
|
+
await commands.cmd_load(args.load)
|
|
1172
1175
|
|
|
1173
1176
|
if args.message:
|
|
1174
1177
|
io.add_to_input_history(args.message)
|
|
1175
1178
|
io.tool_output()
|
|
1176
1179
|
try:
|
|
1177
|
-
coder.run(with_message=args.message)
|
|
1178
|
-
except SwitchCoder:
|
|
1180
|
+
await coder.run(with_message=args.message)
|
|
1181
|
+
except (SwitchCoder, KeyboardInterrupt):
|
|
1179
1182
|
pass
|
|
1180
1183
|
analytics.event("exit", reason="Completed --message")
|
|
1181
1184
|
return
|
|
@@ -1184,7 +1187,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1184
1187
|
try:
|
|
1185
1188
|
message_from_file = io.read_text(args.message_file)
|
|
1186
1189
|
io.tool_output()
|
|
1187
|
-
coder.run(with_message=message_from_file)
|
|
1190
|
+
await coder.run(with_message=message_from_file)
|
|
1188
1191
|
except FileNotFoundError:
|
|
1189
1192
|
io.tool_error(f"Message file not found: {args.message_file}")
|
|
1190
1193
|
analytics.event("exit", reason="Message file not found")
|
|
@@ -1206,7 +1209,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1206
1209
|
while True:
|
|
1207
1210
|
try:
|
|
1208
1211
|
coder.ok_to_warm_cache = bool(args.cache_keepalive_pings)
|
|
1209
|
-
coder.run()
|
|
1212
|
+
await coder.run()
|
|
1210
1213
|
analytics.event("exit", reason="Completed main CLI coder.run")
|
|
1211
1214
|
return
|
|
1212
1215
|
except SwitchCoder as switch:
|
|
@@ -1224,10 +1227,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1224
1227
|
# Disable cache warming for the new coder
|
|
1225
1228
|
kwargs["num_cache_warming_pings"] = 0
|
|
1226
1229
|
|
|
1227
|
-
coder = Coder.create(**kwargs)
|
|
1230
|
+
coder = await Coder.create(**kwargs)
|
|
1228
1231
|
|
|
1229
|
-
if switch.kwargs.get("show_announcements") is
|
|
1230
|
-
coder.
|
|
1232
|
+
if switch.kwargs.get("show_announcements") is False:
|
|
1233
|
+
coder.suppress_announcements_for_next_prompt = True
|
|
1231
1234
|
|
|
1232
1235
|
|
|
1233
1236
|
def is_first_run_of_new_version(io, verbose=False):
|
aider/mcp/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
|
-
from aider.mcp.server import HttpStreamingServer, McpServer
|
|
4
|
+
from aider.mcp.server import HttpStreamingServer, McpServer, SseServer
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def _parse_mcp_servers_from_json_string(json_string, io, verbose=False, mcp_transport="stdio"):
|
|
@@ -24,6 +25,8 @@ def _parse_mcp_servers_from_json_string(json_string, io, verbose=False, mcp_tran
|
|
|
24
25
|
servers.append(McpServer(server_config))
|
|
25
26
|
elif transport == "http":
|
|
26
27
|
servers.append(HttpStreamingServer(server_config))
|
|
28
|
+
elif transport == "sse":
|
|
29
|
+
servers.append(SseServer(server_config))
|
|
27
30
|
|
|
28
31
|
if verbose:
|
|
29
32
|
io.tool_output(f"Loaded {len(servers)} MCP servers from JSON string")
|
|
@@ -38,12 +41,72 @@ def _parse_mcp_servers_from_json_string(json_string, io, verbose=False, mcp_tran
|
|
|
38
41
|
return servers
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
def _resolve_mcp_config_path(file_path, io, verbose=False):
|
|
45
|
+
"""Resolve MCP config file path relative to closest aider.conf.yml, git directory, or CWD."""
|
|
46
|
+
if not file_path:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# If the path is absolute or already exists, use it as-is
|
|
50
|
+
path = Path(file_path)
|
|
51
|
+
if path.is_absolute() or path.exists():
|
|
52
|
+
return str(path.resolve())
|
|
53
|
+
|
|
54
|
+
# Search for the closest aider.conf.yml in parent directories
|
|
55
|
+
current_dir = Path.cwd()
|
|
56
|
+
aider_conf_path = None
|
|
57
|
+
|
|
58
|
+
for parent in [current_dir] + list(current_dir.parents):
|
|
59
|
+
conf_file = parent / ".aider.conf.yml"
|
|
60
|
+
if conf_file.exists():
|
|
61
|
+
aider_conf_path = parent
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
# If aider.conf.yml found, try relative to that directory
|
|
65
|
+
if aider_conf_path:
|
|
66
|
+
resolved_path = aider_conf_path / file_path
|
|
67
|
+
if resolved_path.exists():
|
|
68
|
+
if verbose:
|
|
69
|
+
io.tool_output(f"Resolved MCP config relative to aider.conf.yml: {resolved_path}")
|
|
70
|
+
return str(resolved_path.resolve())
|
|
71
|
+
|
|
72
|
+
# Try to find git root directory
|
|
73
|
+
git_root = None
|
|
74
|
+
try:
|
|
75
|
+
import git
|
|
76
|
+
|
|
77
|
+
repo = git.Repo(search_parent_directories=True)
|
|
78
|
+
git_root = Path(repo.working_tree_dir)
|
|
79
|
+
except (ImportError, git.InvalidGitRepositoryError, FileNotFoundError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# If git root found, try relative to that directory
|
|
83
|
+
if git_root:
|
|
84
|
+
resolved_path = git_root / file_path
|
|
85
|
+
if resolved_path.exists():
|
|
86
|
+
if verbose:
|
|
87
|
+
io.tool_output(f"Resolved MCP config relative to git root: {resolved_path}")
|
|
88
|
+
return str(resolved_path.resolve())
|
|
89
|
+
|
|
90
|
+
# Finally, try relative to current working directory
|
|
91
|
+
resolved_path = current_dir / file_path
|
|
92
|
+
if resolved_path.exists():
|
|
93
|
+
if verbose:
|
|
94
|
+
io.tool_output(f"Resolved MCP config relative to CWD: {resolved_path}")
|
|
95
|
+
return str(resolved_path.resolve())
|
|
96
|
+
|
|
97
|
+
# If none found, return the original path (will trigger FileNotFoundError)
|
|
98
|
+
return str(path.resolve())
|
|
99
|
+
|
|
100
|
+
|
|
41
101
|
def _parse_mcp_servers_from_file(file_path, io, verbose=False, mcp_transport="stdio"):
|
|
42
102
|
"""Parse MCP servers from a JSON file."""
|
|
43
103
|
servers = []
|
|
44
104
|
|
|
105
|
+
# Resolve the file path relative to closest aider.conf.yml, git directory, or CWD
|
|
106
|
+
resolved_file_path = _resolve_mcp_config_path(file_path, io, verbose)
|
|
107
|
+
|
|
45
108
|
try:
|
|
46
|
-
with open(
|
|
109
|
+
with open(resolved_file_path, "r") as f:
|
|
47
110
|
config = json.load(f)
|
|
48
111
|
|
|
49
112
|
if verbose:
|
aider/mcp/server.py
CHANGED
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
from contextlib import AsyncExitStack
|
|
5
5
|
|
|
6
6
|
from mcp import ClientSession, StdioServerParameters
|
|
7
|
+
from mcp.client.sse import sse_client
|
|
7
8
|
from mcp.client.stdio import stdio_client
|
|
8
9
|
from mcp.client.streamable_http import streamablehttp_client
|
|
9
10
|
|
|
@@ -13,12 +14,7 @@ class McpServer:
|
|
|
13
14
|
A client for MCP servers that provides tools to Aider coders. An McpServer class
|
|
14
15
|
is initialized per configured MCP Server
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
conn = await session.connect() # Use connect() directly
|
|
19
|
-
tools = await experimental_mcp_client.load_mcp_tools(session=s, format="openai")
|
|
20
|
-
await session.disconnect()
|
|
21
|
-
print(tools)
|
|
17
|
+
Uses the mcp library to create and initialize ClientSession objects.
|
|
22
18
|
"""
|
|
23
19
|
|
|
24
20
|
def __init__(self, server_config):
|
|
@@ -72,21 +68,25 @@ class McpServer:
|
|
|
72
68
|
try:
|
|
73
69
|
await self.exit_stack.aclose()
|
|
74
70
|
self.session = None
|
|
75
|
-
self.stdio_context = None
|
|
76
71
|
except Exception as e:
|
|
77
72
|
logging.error(f"Error during cleanup of server {self.name}: {e}")
|
|
78
73
|
|
|
79
74
|
|
|
80
75
|
class HttpStreamingServer(McpServer):
|
|
76
|
+
"""HTTP streaming MCP server using mcp.client.streamablehttp_client."""
|
|
77
|
+
|
|
81
78
|
async def connect(self):
|
|
82
79
|
if self.session is not None:
|
|
83
80
|
logging.info(f"Using existing session for MCP server: {self.name}")
|
|
84
81
|
return self.session
|
|
85
82
|
|
|
86
|
-
logging.info(f"Establishing new connection to MCP server: {self.name}")
|
|
83
|
+
logging.info(f"Establishing new connection to HTTP MCP server: {self.name}")
|
|
87
84
|
try:
|
|
88
|
-
url = self.config
|
|
89
|
-
|
|
85
|
+
url = self.config.get("url")
|
|
86
|
+
headers = self.config.get("headers", {})
|
|
87
|
+
http_transport = await self.exit_stack.enter_async_context(
|
|
88
|
+
streamablehttp_client(url, headers=headers)
|
|
89
|
+
)
|
|
90
90
|
read, write, _response = http_transport
|
|
91
91
|
|
|
92
92
|
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
|
@@ -94,7 +94,33 @@ class HttpStreamingServer(McpServer):
|
|
|
94
94
|
self.session = session
|
|
95
95
|
return session
|
|
96
96
|
except Exception as e:
|
|
97
|
-
logging.error(f"Error initializing server {self.name}: {e}")
|
|
97
|
+
logging.error(f"Error initializing HTTP server {self.name}: {e}")
|
|
98
|
+
await self.disconnect()
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SseServer(McpServer):
|
|
103
|
+
"""SSE (Server-Sent Events) MCP server using mcp.client.sse_client."""
|
|
104
|
+
|
|
105
|
+
async def connect(self):
|
|
106
|
+
if self.session is not None:
|
|
107
|
+
logging.info(f"Using existing session for SSE MCP server: {self.name}")
|
|
108
|
+
return self.session
|
|
109
|
+
|
|
110
|
+
logging.info(f"Establishing new connection to SSE MCP server: {self.name}")
|
|
111
|
+
try:
|
|
112
|
+
url = self.config.get("url")
|
|
113
|
+
headers = self.config.get("headers", {})
|
|
114
|
+
sse_transport = await self.exit_stack.enter_async_context(
|
|
115
|
+
sse_client(url, headers=headers)
|
|
116
|
+
)
|
|
117
|
+
read, write, _response = sse_transport
|
|
118
|
+
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
|
119
|
+
await session.initialize()
|
|
120
|
+
self.session = session
|
|
121
|
+
return session
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logging.error(f"Error initializing SSE server {self.name}: {e}")
|
|
98
124
|
await self.disconnect()
|
|
99
125
|
raise
|
|
100
126
|
|
aider/models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import difflib
|
|
2
3
|
import hashlib
|
|
3
4
|
import importlib.resources
|
|
@@ -893,23 +894,33 @@ class Model(ModelSettings):
|
|
|
893
894
|
return self.extra_params["extra_body"]["reasoning_effort"]
|
|
894
895
|
return None
|
|
895
896
|
|
|
896
|
-
def
|
|
897
|
+
def is_deepseek(self):
|
|
897
898
|
name = self.name.lower()
|
|
898
899
|
if "deepseek" not in name:
|
|
899
900
|
return
|
|
900
|
-
return
|
|
901
|
+
return True
|
|
901
902
|
|
|
902
903
|
def is_ollama(self):
|
|
903
904
|
return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/")
|
|
904
905
|
|
|
905
|
-
def send_completion(
|
|
906
|
+
async def send_completion(
|
|
906
907
|
self, messages, functions, stream, temperature=None, tools=None, max_tokens=None
|
|
907
908
|
):
|
|
908
909
|
if os.environ.get("AIDER_SANITY_CHECK_TURNS"):
|
|
909
910
|
sanity_check_messages(messages)
|
|
910
911
|
|
|
911
|
-
|
|
912
|
-
|
|
912
|
+
messages = ensure_alternating_roles(messages)
|
|
913
|
+
|
|
914
|
+
if self.verbose:
|
|
915
|
+
for message in messages:
|
|
916
|
+
msg_role = message.get("role")
|
|
917
|
+
msg_content = message.get("content") if message.get("content") else ""
|
|
918
|
+
msg_trunc = ""
|
|
919
|
+
|
|
920
|
+
if message.get("content"):
|
|
921
|
+
msg_trunc = message.get("content")[:30]
|
|
922
|
+
|
|
923
|
+
print(f"{msg_role} ({len(msg_content)}): {msg_trunc}")
|
|
913
924
|
|
|
914
925
|
kwargs = dict(model=self.name, stream=stream)
|
|
915
926
|
|
|
@@ -923,26 +934,22 @@ class Model(ModelSettings):
|
|
|
923
934
|
kwargs["temperature"] = temperature
|
|
924
935
|
|
|
925
936
|
# `tools` is for modern tool usage. `functions` is for legacy/forced calls.
|
|
926
|
-
# If `tools` is provided, it's the canonical list. If not, use `functions`.
|
|
927
937
|
# This handles `base_coder` sending both with same content for `navigator_coder`.
|
|
928
|
-
effective_tools = tools
|
|
938
|
+
effective_tools = tools
|
|
939
|
+
|
|
940
|
+
if effective_tools is None and functions:
|
|
941
|
+
# Convert legacy `functions` to `tools` format if `tools` isn't provided.
|
|
942
|
+
effective_tools = [dict(type="function", function=f) for f in functions]
|
|
929
943
|
|
|
930
944
|
if effective_tools:
|
|
931
|
-
|
|
932
|
-
# This is a simplifying assumption that works for aider's use cases.
|
|
933
|
-
is_legacy = any("type" not in tool for tool in effective_tools)
|
|
934
|
-
if is_legacy:
|
|
935
|
-
kwargs["tools"] = [dict(type="function", function=tool) for tool in effective_tools]
|
|
936
|
-
else:
|
|
937
|
-
kwargs["tools"] = effective_tools
|
|
945
|
+
kwargs["tools"] = effective_tools
|
|
938
946
|
|
|
939
947
|
# Forcing a function call is for legacy style `functions` with a single function.
|
|
940
948
|
# This is used by ArchitectCoder and not intended for NavigatorCoder's tools.
|
|
941
949
|
if functions and len(functions) == 1:
|
|
942
950
|
function = functions[0]
|
|
943
|
-
is_legacy = "type" not in function
|
|
944
951
|
|
|
945
|
-
if
|
|
952
|
+
if "name" in function:
|
|
946
953
|
tool_name = function.get("name")
|
|
947
954
|
if tool_name:
|
|
948
955
|
kwargs["tool_choice"] = {"type": "function", "function": {"name": tool_name}}
|
|
@@ -978,16 +985,18 @@ class Model(ModelSettings):
|
|
|
978
985
|
}
|
|
979
986
|
|
|
980
987
|
try:
|
|
981
|
-
res = litellm.
|
|
988
|
+
res = await litellm.acompletion(**kwargs)
|
|
982
989
|
except Exception as err:
|
|
983
|
-
|
|
990
|
+
print(f"LiteLLM API Error: {str(err)}")
|
|
991
|
+
res = self.model_error_response()
|
|
984
992
|
|
|
985
993
|
if self.verbose:
|
|
986
994
|
print(f"LiteLLM API Error: {str(err)}")
|
|
995
|
+
raise
|
|
987
996
|
|
|
988
997
|
return hash_object, res
|
|
989
998
|
|
|
990
|
-
def simple_send_with_retries(self, messages, max_tokens=None):
|
|
999
|
+
async def simple_send_with_retries(self, messages, max_tokens=None):
|
|
991
1000
|
from aider.exceptions import LiteLLMExceptions
|
|
992
1001
|
|
|
993
1002
|
litellm_ex = LiteLLMExceptions()
|
|
@@ -1000,7 +1009,7 @@ class Model(ModelSettings):
|
|
|
1000
1009
|
|
|
1001
1010
|
while True:
|
|
1002
1011
|
try:
|
|
1003
|
-
_hash, response = self.send_completion(
|
|
1012
|
+
_hash, response = await self.send_completion(
|
|
1004
1013
|
messages=messages,
|
|
1005
1014
|
functions=None,
|
|
1006
1015
|
stream=False,
|
|
@@ -1031,6 +1040,22 @@ class Model(ModelSettings):
|
|
|
1031
1040
|
except AttributeError:
|
|
1032
1041
|
return None
|
|
1033
1042
|
|
|
1043
|
+
async def model_error_response(self):
|
|
1044
|
+
for i in range(1):
|
|
1045
|
+
await asyncio.sleep(0.1)
|
|
1046
|
+
yield litellm.ModelResponse(
|
|
1047
|
+
choices=[
|
|
1048
|
+
litellm.Choices(
|
|
1049
|
+
finish_reason="stop",
|
|
1050
|
+
index=0,
|
|
1051
|
+
message=litellm.Message(
|
|
1052
|
+
content="Model API Response Error. Please retry the previous request"
|
|
1053
|
+
), # Provide an empty message object
|
|
1054
|
+
)
|
|
1055
|
+
],
|
|
1056
|
+
model=self.name,
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1034
1059
|
|
|
1035
1060
|
def register_models(model_settings_fnames):
|
|
1036
1061
|
files_loaded = []
|
aider/onboarding.py
CHANGED
|
@@ -76,7 +76,7 @@ def try_to_select_default_model():
|
|
|
76
76
|
return None
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def offer_openrouter_oauth(io, analytics):
|
|
79
|
+
async def offer_openrouter_oauth(io, analytics):
|
|
80
80
|
"""
|
|
81
81
|
Offers OpenRouter OAuth flow to the user if no API keys are found.
|
|
82
82
|
|
|
@@ -90,7 +90,7 @@ def offer_openrouter_oauth(io, analytics):
|
|
|
90
90
|
# No API keys found - Offer OpenRouter OAuth
|
|
91
91
|
io.tool_output("OpenRouter provides free and paid access to many LLMs.")
|
|
92
92
|
# Use confirm_ask which handles non-interactive cases
|
|
93
|
-
if io.confirm_ask(
|
|
93
|
+
if await io.confirm_ask(
|
|
94
94
|
"Login to OpenRouter or create a free account?",
|
|
95
95
|
default="y",
|
|
96
96
|
):
|
|
@@ -113,7 +113,7 @@ def offer_openrouter_oauth(io, analytics):
|
|
|
113
113
|
return False
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def select_default_model(args, io, analytics):
|
|
116
|
+
async def select_default_model(args, io, analytics):
|
|
117
117
|
"""
|
|
118
118
|
Selects a default model based on available API keys if no model is specified.
|
|
119
119
|
Offers OAuth flow for OpenRouter if no keys are found.
|
|
@@ -139,7 +139,7 @@ def select_default_model(args, io, analytics):
|
|
|
139
139
|
io.tool_warning(no_model_msg)
|
|
140
140
|
|
|
141
141
|
# Try OAuth if no model was detected
|
|
142
|
-
offer_openrouter_oauth(io, analytics)
|
|
142
|
+
await offer_openrouter_oauth(io, analytics)
|
|
143
143
|
|
|
144
144
|
# Check again after potential OAuth success
|
|
145
145
|
model = try_to_select_default_model()
|
aider/repo.py
CHANGED
|
@@ -21,7 +21,7 @@ import pathspec
|
|
|
21
21
|
from aider import prompts, utils
|
|
22
22
|
|
|
23
23
|
from .dump import dump # noqa: F401
|
|
24
|
-
from .waiting import
|
|
24
|
+
from .waiting import Spinner
|
|
25
25
|
|
|
26
26
|
ANY_GIT_ERROR += [
|
|
27
27
|
OSError,
|
|
@@ -128,7 +128,7 @@ class GitRepo:
|
|
|
128
128
|
if aider_ignore_file:
|
|
129
129
|
self.aider_ignore_file = Path(aider_ignore_file)
|
|
130
130
|
|
|
131
|
-
def commit(self, fnames=None, context=None, message=None, aider_edits=False, coder=None):
|
|
131
|
+
async def commit(self, fnames=None, context=None, message=None, aider_edits=False, coder=None):
|
|
132
132
|
"""
|
|
133
133
|
Commit the specified files or all dirty files if none are specified.
|
|
134
134
|
|
|
@@ -213,7 +213,7 @@ class GitRepo:
|
|
|
213
213
|
user_language = coder.commit_language
|
|
214
214
|
if not user_language:
|
|
215
215
|
user_language = coder.get_user_language()
|
|
216
|
-
commit_message = self.get_commit_message(diffs, context, user_language)
|
|
216
|
+
commit_message = await self.get_commit_message(diffs, context, user_language)
|
|
217
217
|
|
|
218
218
|
# Retrieve attribute settings, prioritizing coder.args if available
|
|
219
219
|
if coder and hasattr(coder, "args"):
|
|
@@ -323,7 +323,7 @@ class GitRepo:
|
|
|
323
323
|
except (ValueError, OSError):
|
|
324
324
|
return self.repo.git_dir
|
|
325
325
|
|
|
326
|
-
def get_commit_message(self, diffs, context, user_language=None):
|
|
326
|
+
async def get_commit_message(self, diffs, context, user_language=None):
|
|
327
327
|
diffs = "# Diffs:\n" + diffs
|
|
328
328
|
|
|
329
329
|
content = ""
|
|
@@ -340,8 +340,8 @@ class GitRepo:
|
|
|
340
340
|
|
|
341
341
|
commit_message = None
|
|
342
342
|
for model in self.models:
|
|
343
|
-
spinner_text = f"Generating commit message with {model.name}"
|
|
344
|
-
with
|
|
343
|
+
spinner_text = f"Generating commit message with {model.name}\n"
|
|
344
|
+
with Spinner(spinner_text):
|
|
345
345
|
if model.system_prompt_prefix:
|
|
346
346
|
current_system_content = model.system_prompt_prefix + "\n" + system_content
|
|
347
347
|
else:
|
|
@@ -358,7 +358,7 @@ class GitRepo:
|
|
|
358
358
|
if max_tokens and num_tokens > max_tokens:
|
|
359
359
|
continue
|
|
360
360
|
|
|
361
|
-
commit_message = model.simple_send_with_retries(messages)
|
|
361
|
+
commit_message = await model.simple_send_with_retries(messages)
|
|
362
362
|
if commit_message:
|
|
363
363
|
break # Found a model that could generate the message
|
|
364
364
|
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"litellm_provider": "fireworks_ai",
|
|
89
89
|
"input_cost_per_token": 0.000008,
|
|
90
90
|
"output_cost_per_token": 0.000008,
|
|
91
|
-
"mode": "chat"
|
|
91
|
+
"mode": "chat"
|
|
92
92
|
},
|
|
93
93
|
"fireworks_ai/accounts/fireworks/models/deepseek-v3-0324": {
|
|
94
94
|
"max_tokens": 160000,
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"litellm_provider": "fireworks_ai",
|
|
98
98
|
"input_cost_per_token": 0.0000009,
|
|
99
99
|
"output_cost_per_token": 0.0000009,
|
|
100
|
-
"mode": "chat"
|
|
100
|
+
"mode": "chat"
|
|
101
101
|
},
|
|
102
102
|
"openrouter/openrouter/quasar-alpha": {
|
|
103
103
|
"max_input_tokens": 1000000,
|
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
"supported_output_modalities": [
|
|
553
553
|
"text"
|
|
554
554
|
],
|
|
555
|
-
"source": "https://ai.google.dev/gemini-api/docs/
|
|
555
|
+
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro"
|
|
556
556
|
},
|
|
557
557
|
"gemini-2.5-pro-preview-06-05": {
|
|
558
558
|
"max_tokens": 65536,
|
|
@@ -592,7 +592,7 @@
|
|
|
592
592
|
"supported_output_modalities": [
|
|
593
593
|
"text"
|
|
594
594
|
],
|
|
595
|
-
"source": "https://ai.google.dev/gemini-api/docs/
|
|
595
|
+
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro"
|
|
596
596
|
},
|
|
597
597
|
"gemini/gemini-2.5-pro-preview-05-06": {
|
|
598
598
|
"max_tokens": 65536,
|
|
@@ -628,7 +628,7 @@
|
|
|
628
628
|
"supported_output_modalities": [
|
|
629
629
|
"text"
|
|
630
630
|
],
|
|
631
|
-
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro
|
|
631
|
+
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro"
|
|
632
632
|
},
|
|
633
633
|
"gemini/gemini-2.5-pro-preview-06-05": {
|
|
634
634
|
"max_tokens": 65536,
|
|
@@ -664,7 +664,7 @@
|
|
|
664
664
|
"supported_output_modalities": [
|
|
665
665
|
"text"
|
|
666
666
|
],
|
|
667
|
-
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro
|
|
667
|
+
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro"
|
|
668
668
|
},
|
|
669
669
|
"gemini/gemini-2.5-pro": {
|
|
670
670
|
"max_tokens": 65536,
|
|
@@ -771,6 +771,6 @@
|
|
|
771
771
|
},
|
|
772
772
|
"together_ai/Qwen/Qwen3-235B-A22B-fp8-tput": {
|
|
773
773
|
"input_cost_per_token": 0.0000002,
|
|
774
|
-
"output_cost_per_token": 0.0000006
|
|
774
|
+
"output_cost_per_token": 0.0000006
|
|
775
775
|
}
|
|
776
|
-
}
|
|
776
|
+
}
|
aider/scrape.py
CHANGED
|
@@ -37,7 +37,7 @@ def has_playwright():
|
|
|
37
37
|
return has_pip and has_chromium
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def install_playwright(io):
|
|
40
|
+
async def install_playwright(io):
|
|
41
41
|
has_pip, has_chromium = check_env()
|
|
42
42
|
if has_pip and has_chromium:
|
|
43
43
|
return True
|
|
@@ -59,7 +59,7 @@ See {urls.enable_playwright} for more info.
|
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
61
|
io.tool_output(text)
|
|
62
|
-
if not io.confirm_ask("Install playwright?", default="y"):
|
|
62
|
+
if not await io.confirm_ask("Install playwright?", default="y"):
|
|
63
63
|
return
|
|
64
64
|
|
|
65
65
|
if not has_pip:
|