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.

Files changed (60) hide show
  1. aider/__init__.py +1 -1
  2. aider/_version.py +2 -2
  3. aider/args.py +6 -0
  4. aider/coders/architect_coder.py +3 -3
  5. aider/coders/base_coder.py +505 -184
  6. aider/coders/context_coder.py +1 -1
  7. aider/coders/editblock_func_coder.py +2 -2
  8. aider/coders/navigator_coder.py +451 -649
  9. aider/coders/navigator_legacy_prompts.py +49 -284
  10. aider/coders/navigator_prompts.py +46 -473
  11. aider/coders/search_replace.py +0 -0
  12. aider/coders/wholefile_func_coder.py +2 -2
  13. aider/commands.py +56 -44
  14. aider/history.py +14 -12
  15. aider/io.py +354 -117
  16. aider/llm.py +12 -4
  17. aider/main.py +22 -19
  18. aider/mcp/__init__.py +65 -2
  19. aider/mcp/server.py +37 -11
  20. aider/models.py +45 -20
  21. aider/onboarding.py +4 -4
  22. aider/repo.py +7 -7
  23. aider/resources/model-metadata.json +8 -8
  24. aider/scrape.py +2 -2
  25. aider/sendchat.py +185 -15
  26. aider/tools/__init__.py +44 -23
  27. aider/tools/command.py +18 -0
  28. aider/tools/command_interactive.py +18 -0
  29. aider/tools/delete_block.py +23 -0
  30. aider/tools/delete_line.py +19 -1
  31. aider/tools/delete_lines.py +20 -1
  32. aider/tools/extract_lines.py +25 -2
  33. aider/tools/git.py +142 -0
  34. aider/tools/grep.py +47 -2
  35. aider/tools/indent_lines.py +25 -0
  36. aider/tools/insert_block.py +26 -0
  37. aider/tools/list_changes.py +15 -0
  38. aider/tools/ls.py +24 -1
  39. aider/tools/make_editable.py +18 -0
  40. aider/tools/make_readonly.py +19 -0
  41. aider/tools/remove.py +22 -0
  42. aider/tools/replace_all.py +21 -0
  43. aider/tools/replace_line.py +20 -1
  44. aider/tools/replace_lines.py +21 -1
  45. aider/tools/replace_text.py +22 -0
  46. aider/tools/show_numbered_context.py +18 -0
  47. aider/tools/undo_change.py +15 -0
  48. aider/tools/update_todo_list.py +131 -0
  49. aider/tools/view.py +23 -0
  50. aider/tools/view_files_at_glob.py +32 -27
  51. aider/tools/view_files_matching.py +51 -37
  52. aider/tools/view_files_with_symbol.py +41 -54
  53. aider/tools/view_todo_list.py +57 -0
  54. aider/waiting.py +20 -203
  55. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
  56. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
  57. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
  58. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
  59. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
  60. {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 main(argv, input, output, right_repo_root, return_coder=return_coder)
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 not False:
1230
- coder.show_announcements()
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(file_path, "r") as f:
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
- Current usage:
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["url"]
89
- http_transport = await self.exit_stack.enter_async_context(streamablehttp_client(url))
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 is_deepseek_r1(self):
897
+ def is_deepseek(self):
897
898
  name = self.name.lower()
898
899
  if "deepseek" not in name:
899
900
  return
900
- return "r1" in name or "reasoner" in name
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
- if self.is_deepseek_r1():
912
- messages = ensure_alternating_roles(messages)
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 if tools is not None else functions
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
- # Check if we have legacy format functions (which lack a 'type' key) and convert them.
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 is_legacy and "name" in function:
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.completion(**kwargs)
988
+ res = await litellm.acompletion(**kwargs)
982
989
  except Exception as err:
983
- res = "Model API Response Error. Please retry the previous request"
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 WaitingSpinner
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 WaitingSpinner(spinner_text):
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/models#gemini-2.5-flash-preview"
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/models#gemini-2.5-flash-preview"
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-preview"
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-preview"
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: