aider-ce 0.87.13.dev3__py3-none-any.whl → 0.88.1__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 +511 -190
- 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/exceptions.py +1 -0
- aider/history.py +14 -12
- aider/io.py +354 -117
- aider/llm.py +12 -4
- aider/main.py +32 -29
- aider/mcp/__init__.py +65 -2
- aider/mcp/server.py +37 -11
- aider/models.py +45 -20
- aider/onboarding.py +5 -5
- 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.dev3.dist-info → aider_ce-0.88.1.dist-info}/METADATA +21 -5
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/RECORD +60 -57
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/WHEEL +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/entry_points.txt +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/licenses/LICENSE.txt +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.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
|
|
@@ -411,7 +412,7 @@ def register_litellm_models(git_root, model_metadata_fname, io, verbose=False):
|
|
|
411
412
|
return 1
|
|
412
413
|
|
|
413
414
|
|
|
414
|
-
def sanity_check_repo(repo, io):
|
|
415
|
+
async def sanity_check_repo(repo, io):
|
|
415
416
|
if not repo:
|
|
416
417
|
return True
|
|
417
418
|
|
|
@@ -442,7 +443,7 @@ def sanity_check_repo(repo, io):
|
|
|
442
443
|
io.tool_error("Aider only works with git repos with version number 1 or 2.")
|
|
443
444
|
io.tool_output("You may be able to convert your repo: git update-index --index-version=2")
|
|
444
445
|
io.tool_output("Or run aider --no-git to proceed without using git.")
|
|
445
|
-
io.offer_url(urls.git_index_version, "Open documentation url for more info?")
|
|
446
|
+
await io.offer_url(urls.git_index_version, "Open documentation url for more info?")
|
|
446
447
|
return False
|
|
447
448
|
|
|
448
449
|
io.tool_error("Unable to read git repository, it may be corrupt?")
|
|
@@ -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)
|
|
@@ -778,7 +783,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
778
783
|
io.tool_output(cmd_line, log_only=True)
|
|
779
784
|
|
|
780
785
|
is_first_run = is_first_run_of_new_version(io, verbose=args.verbose)
|
|
781
|
-
check_and_load_imports(io, is_first_run, verbose=args.verbose)
|
|
786
|
+
await check_and_load_imports(io, is_first_run, verbose=args.verbose)
|
|
782
787
|
|
|
783
788
|
register_models(git_root, args.model_settings_file, io, verbose=args.verbose)
|
|
784
789
|
register_litellm_models(git_root, args.model_metadata_file, io, 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"):
|
|
@@ -839,7 +844,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
839
844
|
io.tool_error(
|
|
840
845
|
f"Unable to proceed without an OpenRouter API key for model '{args.model}'."
|
|
841
846
|
)
|
|
842
|
-
io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
|
|
847
|
+
await io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
|
|
843
848
|
analytics.event(
|
|
844
849
|
"exit",
|
|
845
850
|
reason="OpenRouter key missing for specified model and OAuth failed/declined",
|
|
@@ -921,7 +926,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
921
926
|
io.tool_output("You can skip this check with --no-show-model-warnings")
|
|
922
927
|
|
|
923
928
|
try:
|
|
924
|
-
io.offer_url(urls.model_warnings, "Open documentation url for more info?")
|
|
929
|
+
await io.offer_url(urls.model_warnings, "Open documentation url for more info?")
|
|
925
930
|
io.tool_output()
|
|
926
931
|
except KeyboardInterrupt:
|
|
927
932
|
analytics.event("exit", reason="Keyboard interrupt during model warnings")
|
|
@@ -949,7 +954,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
949
954
|
pass
|
|
950
955
|
|
|
951
956
|
if not args.skip_sanity_check_repo:
|
|
952
|
-
if not sanity_check_repo(repo, io):
|
|
957
|
+
if not await sanity_check_repo(repo, io):
|
|
953
958
|
analytics.event("exit", reason="Repository sanity check failed")
|
|
954
959
|
return 1
|
|
955
960
|
|
|
@@ -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,
|
|
@@ -1054,7 +1059,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1054
1059
|
)
|
|
1055
1060
|
except UnknownEditFormat as err:
|
|
1056
1061
|
io.tool_error(str(err))
|
|
1057
|
-
io.offer_url(urls.edit_formats, "Open documentation about edit formats?")
|
|
1062
|
+
await io.offer_url(urls.edit_formats, "Open documentation about edit formats?")
|
|
1058
1063
|
analytics.event("exit", reason="Unknown edit format")
|
|
1059
1064
|
return 1
|
|
1060
1065
|
except ValueError as err:
|
|
@@ -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
|
|
|
@@ -1149,7 +1152,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|
|
1149
1152
|
webbrowser.open(urls.release_notes)
|
|
1150
1153
|
elif args.show_release_notes is None and is_first_run:
|
|
1151
1154
|
io.tool_output()
|
|
1152
|
-
io.offer_url(
|
|
1155
|
+
await io.offer_url(
|
|
1153
1156
|
urls.release_notes,
|
|
1154
1157
|
"Would you like to see what's new in this version?",
|
|
1155
1158
|
allow_never=False,
|
|
@@ -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):
|
|
@@ -1273,7 +1276,7 @@ def is_first_run_of_new_version(io, verbose=False):
|
|
|
1273
1276
|
return True # Safer to assume it's a first run if we hit an error
|
|
1274
1277
|
|
|
1275
1278
|
|
|
1276
|
-
def check_and_load_imports(io, is_first_run, verbose=False):
|
|
1279
|
+
async def check_and_load_imports(io, is_first_run, verbose=False):
|
|
1277
1280
|
try:
|
|
1278
1281
|
if is_first_run:
|
|
1279
1282
|
if verbose:
|
|
@@ -1285,7 +1288,7 @@ def check_and_load_imports(io, is_first_run, verbose=False):
|
|
|
1285
1288
|
except Exception as err:
|
|
1286
1289
|
io.tool_error(str(err))
|
|
1287
1290
|
io.tool_output("Error loading required imports. Did you install aider properly?")
|
|
1288
|
-
io.offer_url(urls.install_properly, "Open documentation url for more info?")
|
|
1291
|
+
await io.offer_url(urls.install_properly, "Open documentation url for more info?")
|
|
1289
1292
|
sys.exit(1)
|
|
1290
1293
|
|
|
1291
1294
|
if verbose:
|
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,14 +139,14 @@ 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()
|
|
146
146
|
if model:
|
|
147
147
|
return model
|
|
148
148
|
|
|
149
|
-
io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
|
|
149
|
+
await io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
# Helper function to find an available port
|
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
|
|