diff --git a/HISTORY.md b/HISTORY.md index 7d14ebd0e..bbbf1b826 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,17 @@ # Release history +### Aider v0.63.0 + +- Support for Qwen 2.5 Coder 32B. +- `/web` command just adds the page to the chat, without triggering an LLM response. +- Improved prompting for the user's preferred chat language. +- Improved handling of LiteLLM exceptions. +- Bugfix for double-counting tokens when reporting cache stats. +- Bugfix for the LLM creating new files. +- Other small bug fixes. +- Aider wrote 55% of the code in this release. + ### Aider v0.62.0 - Full support for Claude 3.5 Haiku diff --git a/aider/__init__.py b/aider/__init__.py index 362e01532..be5c29480 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ try: from aider.__version__ import __version__ except Exception: - __version__ = "0.62.2.dev" + __version__ = "0.63.1.dev" __all__ = [__version__] diff --git a/aider/coders/architect_coder.py b/aider/coders/architect_coder.py index eece96a89..a561e3e0d 100644 --- a/aider/coders/architect_coder.py +++ b/aider/coders/architect_coder.py @@ -10,6 +10,9 @@ class ArchitectCoder(AskCoder): def reply_completed(self): content = self.partial_response_content + if not content or not content.strip(): + return + if not self.io.confirm_ask("Edit the files?"): return diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 164010140..ebd36a941 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -355,6 +355,9 @@ class Coder: for fname in fnames: fname = Path(fname) + if self.repo and self.repo.git_ignored_file(fname): + self.io.tool_warning(f"Skipping {fname} that matches gitignore spec.") + if self.repo and self.repo.ignored_file(fname): self.io.tool_warning(f"Skipping {fname} that matches aiderignore spec.") continue @@ -1242,10 +1245,19 @@ class Coder: else: content = "" - try: - self.reply_completed() - except KeyboardInterrupt: - interrupted = True + if not interrupted: + add_rel_files_message = self.check_for_file_mentions(content) + if add_rel_files_message: + if self.reflected_message: + self.reflected_message += "\n\n" + add_rel_files_message + else: + self.reflected_message = add_rel_files_message + return + + try: + self.reply_completed() + except KeyboardInterrupt: + interrupted = True if interrupted: content += "\n^C KeyboardInterrupt" @@ -1296,13 +1308,6 @@ class Coder: self.update_cur_messages() return - add_rel_files_message = self.check_for_file_mentions(content) - if add_rel_files_message: - if self.reflected_message: - self.reflected_message += "\n\n" + add_rel_files_message - else: - self.reflected_message = add_rel_files_message - def reply_completed(self): pass @@ -1603,7 +1608,6 @@ class Coder: completion.usage, "cache_creation_input_tokens" ): self.message_tokens_sent += prompt_tokens - self.message_tokens_sent += cache_hit_tokens self.message_tokens_sent += cache_write_tokens else: self.message_tokens_sent += prompt_tokens @@ -1780,6 +1784,10 @@ class Coder: self.check_for_dirty_commit(path) return True + if self.repo and self.repo.git_ignored_file(path): + self.io.tool_warning(f"Skipping edits to {path} that matches gitignore spec.") + return + if not Path(full_path).exists(): if not self.io.confirm_ask("Create new file?", subject=path): self.io.tool_output(f"Skipping edits to {path}") diff --git a/aider/commands.py b/aider/commands.py index 91d0ea7c3..5d938098d 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -743,6 +743,10 @@ class Commands: ) continue + if self.coder.repo and self.coder.repo.git_ignored_file(matched_file): + self.io.tool_error(f"Can't add {matched_file} which is in gitignore") + continue + if abs_file_path in self.coder.abs_fnames: self.io.tool_error(f"{matched_file} is already in the chat as an editable file") continue diff --git a/aider/io.py b/aider/io.py index b6bcab3ee..53cdee0a5 100644 --- a/aider/io.py +++ b/aider/io.py @@ -780,13 +780,23 @@ class InputOutput: editable_files = [f for f in sorted(rel_fnames) if f not in rel_read_only_fnames] if read_only_files: - console.print("Read only files:", style="bold") - console.print(Columns(read_only_files)) + files_with_label = ["Readonly:"] + read_only_files + read_only_output = StringIO() + Console(file=read_only_output, force_terminal=False).print(Columns(files_with_label)) + read_only_lines = read_only_output.getvalue().splitlines() + console.print(Columns(files_with_label)) + if editable_files: + files_with_label = editable_files if read_only_files: - console.print() - console.print("Editable files:", style="bold") - console.print(Columns(editable_files)) + files_with_label = ["Editable:"] + editable_files + editable_output = StringIO() + Console(file=editable_output, force_terminal=False).print(Columns(files_with_label)) + editable_lines = editable_output.getvalue().splitlines() + + if len(read_only_lines) > 1 or len(editable_lines) > 1: + console.print() + console.print(Columns(files_with_label)) return output.getvalue() diff --git a/aider/models.py b/aider/models.py index 95a4483a0..57d5599c6 100644 --- a/aider/models.py +++ b/aider/models.py @@ -550,6 +550,11 @@ MODEL_SETTINGS = [ "diff-fenced", use_repo_map=True, ), + ModelSettings( + "vertex_ai/gemini-pro-experimental", + "diff-fenced", + use_repo_map=True, + ), ModelSettings( "gemini/gemini-1.5-flash-exp-0827", "whole", @@ -710,6 +715,14 @@ MODEL_SETTINGS = [ use_temperature=False, streaming=False, ), + ModelSettings( + "openrouter/qwen/qwen-2.5-coder-32b-instruct", + "diff", + weak_model_name="openrouter/qwen/qwen-2.5-coder-32b-instruct", + editor_model_name="openrouter/qwen/qwen-2.5-coder-32b-instruct", + editor_edit_format="editor-diff", + use_repo_map=True, + ), ] @@ -770,16 +783,20 @@ class ModelInfoManager: return dict() def get_model_info(self, model): - if not litellm._lazy_module: - info = self.get_model_from_cached_json_db(model) - if info: - return info + cached_info = self.get_model_from_cached_json_db(model) - # If all else fails, do it the slow way... - try: - return litellm.get_model_info(model) - except Exception: - return dict() + litellm_info = None + if litellm._lazy_module or not cached_info: + try: + litellm_info = litellm.get_model_info(model) + except Exception as ex: + if "model_prices_and_context_window.json" not in str(ex): + print(str(ex)) + + if litellm_info: + return litellm_info + + return cached_info model_info_manager = ModelInfoManager() @@ -863,6 +880,17 @@ class Model(ModelSettings): self.use_temperature = False self.streaming = False + if ( + "qwen" in model + and "coder" in model + and ("2.5" in model or "2-5" in model) + and "32b" in model + ): + "openrouter/qwen/qwen-2.5-coder-32b-instruct", + self.edit_format = "diff" + self.editor_edit_format = "editor-diff" + self.use_repo_map = True + # use the defaults if self.edit_format == "diff": self.use_repo_map = True @@ -1049,8 +1077,14 @@ def register_litellm_models(model_fnames): continue try: - with open(model_fname, "r") as model_def_file: - model_def = json5.load(model_def_file) + data = Path(model_fname).read_text() + if not data.strip(): + continue + model_def = json5.loads(data) + if not model_def: + continue + + # only load litellm if we have actual data litellm._load_litellm() litellm.register_model(model_def) except Exception as e: diff --git a/aider/repo.py b/aider/repo.py index d3ccc07b8..83cb914de 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -169,7 +169,7 @@ class GitRepo: def get_rel_repo_dir(self): try: return os.path.relpath(self.repo.git_dir, os.getcwd()) - except ValueError: + except (ValueError, OSError): return self.repo.git_dir def get_commit_message(self, diffs, context): @@ -331,6 +331,12 @@ class GitRepo: lines, ) + def git_ignored_file(self, path): + if not self.repo: + return + if self.repo.ignored(path): + return True + def ignored_file(self, fname): self.refresh_aider_ignore() diff --git a/aider/resources/model-metadata.json b/aider/resources/model-metadata.json index e3970e0af..a2fd02b12 100644 --- a/aider/resources/model-metadata.json +++ b/aider/resources/model-metadata.json @@ -1,75 +1,11 @@ { - "claude-3-5-haiku-20241022": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "anthropic", - "mode": "chat", - "supports_function_calling": true, - "tool_use_system_prompt_tokens": 264, - "supports_assistant_prefill": true, - "supports_prompt_caching": true - }, - "vertex_ai/claude-3-5-haiku@20241022": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "vertex_ai-anthropic_models", - "mode": "chat", - "supports_function_calling": true, - "supports_assistant_prefill": true - }, - "openrouter/anthropic/claude-3-5-haiku": { - "max_tokens": 200000, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, + "openrouter/qwen/qwen-2.5-coder-32b-instruct": { + "max_tokens": 33792, + "max_input_tokens": 33792, + "max_output_tokens": 33792, + "input_cost_per_token": 0.00000018, + "output_cost_per_token": 0.00000018, "litellm_provider": "openrouter", "mode": "chat", - "supports_function_calling": true, - }, - "openrouter/anthropic/claude-3-5-haiku-20241022": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "openrouter", - "mode": "chat", - "supports_function_calling": true, - "tool_use_system_prompt_tokens": 264 - }, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "bedrock", - "mode": "chat", - "supports_function_calling": true, - }, - "us.anthropic.claude-3-5-haiku-20241022-v1:0": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "bedrock", - "mode": "chat", - "supports_function_calling": true, - }, - "eu.anthropic.claude-3-5-haiku-20241022-v1:0": { - "max_tokens": 4096, - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000001, - "output_cost_per_token": 0.000005, - "litellm_provider": "bedrock", - "mode": "chat", - "supports_function_calling": true, - }, + }, } diff --git a/aider/website/HISTORY.md b/aider/website/HISTORY.md index d0b695077..b7f80fdb4 100644 --- a/aider/website/HISTORY.md +++ b/aider/website/HISTORY.md @@ -10,6 +10,10 @@ description: Release notes and stats on aider writing its own code. {% include blame.md %} +The above +[stats are based on the git commit history](/docs/faq.html#how-are-the-aider-wrote-xx-of-code-stats-computed) +in the aider repo. + diff --git a/aider/website/docs/faq.md b/aider/website/docs/faq.md index f7cf40cfe..9c14e5f21 100644 --- a/aider/website/docs/faq.md +++ b/aider/website/docs/faq.md @@ -150,7 +150,6 @@ python -m aider - ## Can I change the system prompts that aider uses? Aider is set up to support different system prompts and edit formats @@ -191,6 +190,16 @@ You can also refer to the [instructions for installing a development version of aider](https://aider.chat/docs/install/optional.html#install-the-development-version-of-aider). +## How are the "aider wrote xx% of code" stats computed? + +[Aider is tightly integrated with git](/docs/git.html) so all +one of aider's code changes are committed to the repo with proper attribution. +The +[stats are computed](https://github.com/Aider-AI/aider/blob/main/scripts/blame.py) +by doing something like `git blame` on the repo, +and counting up who wrote all the new lines of code in each release. +Only lines in source code files are counted, not documentation or prompt files. + ## Can I share my aider chat transcript? Yes, you can now share aider chat logs in a pretty way. @@ -213,6 +222,15 @@ This will give you a URL like this, which shows the chat history like you'd see https://aider.chat/share/?mdurl=https://gist.github.com/Aider-AI/2087ab8b64034a078c0a209440ac8be0 ``` +## Can I edit files myself while aider is running? + +Yes. Aider always reads the latest copy of files from the file +system when you send each message. + +While you're waiting for aider's reply to complete, it's probably unwise to +edit files that you've added to the chat. +Your edits and aider's edits might conflict. + ## What is Aider AI LLC? Aider AI LLC is the company behind the aider AI coding tool. @@ -222,11 +240,5 @@ under an [Apache 2.0 license](https://github.com/Aider-AI/aider/blob/main/LICENSE.txt). -## Can I edit files myself while aider is running? +
-Yes. Aider always reads the latest copy of files from the file -system when you send each message. - -While you're waiting for aider's reply to complete, it's probably unwise to -edit files that you've added to the chat. -Your edits and aider's edits might conflict. diff --git a/tests/basic/test_commands.py b/tests/basic/test_commands.py index 7288a8d6d..be92049db 100644 --- a/tests/basic/test_commands.py +++ b/tests/basic/test_commands.py @@ -1210,6 +1210,26 @@ class TestCommands(TestCase): del commands del repo + def test_cmd_add_gitignored_file(self): + with GitTemporaryDirectory(): + # Create a .gitignore file + gitignore = Path(".gitignore") + gitignore.write_text("*.ignored\n") + + # Create a file that matches the gitignore pattern + ignored_file = Path("test.ignored") + ignored_file.write_text("This should be ignored") + + io = InputOutput(pretty=False, fancy_input=False, yes=False) + coder = Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Try to add the ignored file + commands.cmd_add(str(ignored_file)) + + # Verify the file was not added + self.assertEqual(len(coder.abs_fnames), 0) + def test_cmd_add_aiderignored_file(self): with GitTemporaryDirectory(): repo = git.Repo()