This commit is contained in:
yozerpp 2025-04-25 18:40:16 +03:00
commit dbdf741264
340 changed files with 22361 additions and 3227 deletions

View file

@ -0,0 +1,86 @@
name: Check PyPI Version
# Check to be sure `pip install aider-chat` installs the most recently published version.
# If dependencies get yanked, it may render the latest version uninstallable.
# See https://github.com/Aider-AI/aider/issues/3699 for example.
on:
schedule:
# Run once a day at midnight UTC
- cron: '0 0 * * *'
workflow_dispatch: # Allows manual triggering
jobs:
check_version:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install aider-chat
run: pip install aider-chat
- name: Get installed aider version
id: installed_version
run: |
set -x # Enable debugging output
aider_version_output=$(aider --version)
if [ $? -ne 0 ]; then
echo "Error: 'aider --version' command failed."
exit 1
fi
echo "Raw aider --version output: $aider_version_output"
# Extract version number (format X.Y.Z)
version_num=$(echo "$aider_version_output" | grep -oP '\d+\.\d+\.\d+')
# Check if grep found anything
if [ -z "$version_num" ]; then
echo "Error: Could not extract version number using grep -oP '\d+\.\d+\.\d+' from output: $aider_version_output"
exit 1
fi
echo "Extracted version number: $version_num"
echo "version=$version_num" >> $GITHUB_OUTPUT
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all tags
- name: Get latest tag
id: latest_tag
run: |
set -x # Enable debugging output
# Fetch all tags from remote just in case
git fetch --tags origin main
# Get the latest tag that strictly matches vX.Y.Z (no suffixes like .dev)
# List all tags, sort by version descending, filter for exact pattern, take the first one
latest_tag=$(git tag --sort=-v:refname | grep -P '^v\d+\.\d+\.\d+$' | head -n 1)
if [ -z "$latest_tag" ]; then
echo "Error: Could not find any tags matching the pattern '^v\d+\.\d+\.\d+$'"
exit 1
fi
echo "Latest non-dev tag: $latest_tag"
# Remove 'v' prefix for comparison
tag_num=${latest_tag#v}
echo "Extracted tag number: $tag_num"
echo "tag=$tag_num" >> $GITHUB_OUTPUT
- name: Compare versions
run: |
echo "Installed version: ${{ steps.installed_version.outputs.version }}"
echo "Latest tag version: ${{ steps.latest_tag.outputs.tag }}"
if [ "${{ steps.installed_version.outputs.version }}" != "${{ steps.latest_tag.outputs.tag }}" ]; then
echo "Error: Installed aider version (${{ steps.installed_version.outputs.version }}) does not match the latest tag (${{ steps.latest_tag.outputs.tag }})."
exit 1
fi
echo "Versions match."

View file

@ -0,0 +1,90 @@
name: Windows Check PyPI Version
# Check to be sure `pip install aider-chat` installs the most recently published version on Windows.
# If dependencies get yanked, it may render the latest version uninstallable.
# See https://github.com/Aider-AI/aider/issues/3699 for example.
on:
schedule:
# Run once a day at 1 AM UTC (offset from Ubuntu check)
- cron: '0 1 * * *'
workflow_dispatch: # Allows manual triggering
jobs:
check_version:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
defaults:
run:
shell: pwsh # Use PowerShell for all run steps
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install aider-chat
run: pip install aider-chat
- name: Get installed aider version
id: installed_version
run: |
Write-Host "Running 'aider --version'..."
$aider_version_output = aider --version
if ($LASTEXITCODE -ne 0) {
Write-Error "Error: 'aider --version' command failed."
exit 1
}
Write-Host "Raw aider --version output: $aider_version_output"
# Extract version number (format X.Y.Z) using PowerShell regex
$match = [regex]::Match($aider_version_output, '\d+\.\d+\.\d+')
if (-not $match.Success) {
Write-Error "Error: Could not extract version number using regex '\d+\.\d+\.\d+' from output: $aider_version_output"
exit 1
}
$version_num = $match.Value
Write-Host "Extracted version number: $version_num"
echo "version=$version_num" >> $env:GITHUB_OUTPUT
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all tags
- name: Get latest tag
id: latest_tag
run: |
Write-Host "Fetching tags..."
# Fetch all tags from remote just in case
git fetch --tags origin main
Write-Host "Getting latest non-dev tag..."
# Get the latest tag that strictly matches vX.Y.Z (no suffixes like .dev)
# List all tags, sort by version descending, filter for exact pattern, take the first one
$latest_tag = (git tag --sort=-v:refname | Select-String -Pattern '^v\d+\.\d+\.\d+$' | Select-Object -First 1).Line
if (-not $latest_tag) {
Write-Error "Error: Could not find any tags matching the pattern '^v\d+\.\d+\.\d+$'"
exit 1
}
Write-Host "Latest non-dev tag: $latest_tag"
# Remove 'v' prefix for comparison
$tag_num = $latest_tag.Substring(1)
Write-Host "Extracted tag number: $tag_num"
echo "tag=$tag_num" >> $env:GITHUB_OUTPUT
- name: Compare versions
run: |
Write-Host "Installed version: ${{ steps.installed_version.outputs.version }}"
Write-Host "Latest tag version: ${{ steps.latest_tag.outputs.tag }}"
if ("${{ steps.installed_version.outputs.version }}" -ne "${{ steps.latest_tag.outputs.tag }}") {
Write-Error "Error: Installed aider version (${{ steps.installed_version.outputs.version }}) does not match the latest tag (${{ steps.latest_tag.outputs.tag }})."
exit 1
}
Write-Host "Versions match."

View file

@ -18,5 +18,6 @@ repos:
rev: v2.2.6
hooks:
- id: codespell
args: ["--skip", "aider/website/docs/languages.md"]
additional_dependencies:
- tomli

View file

@ -2,8 +2,213 @@
### main branch
- Add support for `gemini-2.5-flash-preview-04-17` models.
- Improved "diff" format for Gemini 2.5 Flash by accepting filenames provided on the same line as the opening fence.
- Add new `udiff-simple` edit format, for Gemini 2.5 Pro.
- Update default weak/editor models for Gemini 2.5 Pro models to use `gemini-2.5-flash-preview-04-17`.
- Aider wrote 69% of the code in this release.
### Aider v0.82.2
- Fix editing shell files with diff-fenced, by zjy1412.
- Improve robustness of patch application by allowing multiple update/delete actions for the same file within a single response.
- Update prompts to instruct LLMs to consolidate all edits for a given file into a single block within the patch.
### Aider v0.82.1
- Added support for `o3` and `o4-mini` including provider-specific versions for OpenAI, OpenRouter, and Azure.
- Added support for Azure specific `gpt-4.1` and `gpt-4.1-mini` models.
- Disabled streaming for `o3` models since you need identity verification to stream.
- Fixed handling of file paths in unified diffs, especially those generated by git.
### Aider v0.82.0
- Support for GPT 4.1, mini and nano.
- Added new `patch` edit format for OpenAI's GPT-4.1 model.
- Improved support for using architect mode with Gemini 2.5 Pro.
- Added new `editor-diff`, `editor-whole`, and `editor-diff-fenced` edit formats.
- Bugfix for automatically selecting the best edit format to use in architect mode.
- Added support for `grok-3-fast-beta` and `grok-3-mini-fast-beta` models.
- Aider wrote 92% of the code in this release.
### Aider v0.81.3
- Commit messages generated by aider are no longer forced to be entirely lowercase, by Peter Hadlaw.
- Updated default settings for Grok models.
### Aider v0.81.2
- Add support for `xai/grok-3-beta`, `xai/grok-3-mini-beta`, `openrouter/x-ai/grok-3-beta`, `openrouter/x-ai/grok-3-mini-beta`, and `openrouter/openrouter/optimus-alpha` models.
- Add alias "grok3" for `xai/grok-3-beta`.
- Add alias "optimus" for `openrouter/openrouter/optimus-alpha`.
- Fix URL extraction from error messages.
- Allow adding files by full path even if a file with the same basename is already in the chat.
- Fix quoting of values containing '#' in the sample `aider.conf.yml`.
- Add support for Fireworks AI model 'deepseek-v3-0324', by Felix Lisczyk.
- Commit messages generated by aider are now lowercase, by Anton Ödman.
### Aider v0.81.1
- Added support for the `gemini/gemini-2.5-pro-preview-03-25` model.
- Updated the `gemini` alias to point to `gemini/gemini-2.5-pro-preview-03-25`.
- Added the `gemini-exp` alias for `gemini/gemini-2.5-pro-exp-03-25`.
### Aider v0.81.0
- Added support for the `openrouter/openrouter/quasar-alpha` model.
- Run with `aider --model quasar`
- Offer OpenRouter OAuth authentication if an OpenRouter model is specified but the API key is missing.
- Prevent retrying API calls when the provider reports insufficient credits.
- Improve URL detection to exclude trailing double quotes.
- Aider wrote 86% of the code in this release.
### Aider v0.80.4
- Bumped deps to pickup litellm change to properly display the root cause of OpenRouter "choices" errors.
### Aider v0.80.3
- Improve error message for OpenRouter API connection issues to mention potential rate limiting or upstream provider issues.
- Configure weak models (`gemini/gemini-2.0-flash` and `openrouter/google/gemini-2.0-flash-exp:free`) for Gemini 2.5 Pro models.
- Add model metadata for `openrouter/google/gemini-2.0-flash-exp:free`.
### Aider v0.80.2
- Bumped deps.
### Aider v0.80.1
- Updated deps for yanked fsspec and aiohttp packages #3699
- Removed redundant dependency check during OpenRouter OAuth flow, by Claudia Pellegrino.
### Aider v0.80.0
- OpenRouter OAuth integration:
- Offer to OAuth against OpenRouter if no model and keys are provided.
- Select OpenRouter default model based on free/paid tier status if `OPENROUTER_API_KEY` is set and no model is specified.
- Prioritize `gemini/gemini-2.5-pro-exp-03-25` if `GEMINI_API_KEY` is set, and `vertex_ai/gemini-2.5-pro-exp-03-25` if `VERTEXAI_PROJECT` is set, when no model is specified.
- Validate user-configured color settings on startup and warn/disable invalid ones.
- Warn at startup if `--stream` and `--cache-prompts` are used together, as cost estimates may be inaccurate.
- Boost repomap ranking for files whose path components match identifiers mentioned in the chat.
- Change web scraping timeout from an error to a warning, allowing scraping to continue with potentially incomplete content.
- Left-align markdown headings in the terminal output, by Peter Schilling.
- Update edit format to the new model's default when switching models with `/model`, if the user was using the old model's default format.
- Add `Ctrl-X Ctrl-E` keybinding to edit the current input buffer in an external editor, by Matteo Landi.
- Fix linting errors for filepaths containing shell metacharacters, by Mir Adnan ALI.
- Add the `openrouter/deepseek-chat-v3-0324:free` model.
- Add repomap support for the Scala language, by Vasil Markoukin.
- Fixed bug in `/run` that was preventing auto-testing.
- Fix bug preventing `UnboundLocalError` during git tree traversal.
- Handle `GitCommandNotFound` error if git is not installed or not in PATH.
- Handle `FileNotFoundError` if the current working directory is deleted while aider is running.
- Fix completion menu current item color styling, by Andrey Ivanov.
- Aider wrote 87% of the code in this release.
### Aider v0.79.2
- Added 'gemini' alias for gemini-2.5-pro model.
- Updated Gemini 2.5 Pro max output tokens to 64k.
- Added support for Lisp-style semicolon comments in file watcher, by Matteo Landi.
- Added OpenRouter API error detection and retries.
- Added openrouter/deepseek-chat-v3-0324 model.
- Aider wrote 93% of the code in this release.
### Aider v0.79.1
- Improved model listing to include all models in fuzzy matching, including those provided by aider (not litellm).
### Aider v0.79.0
- Added support for Gemini 2.5 Pro models.
- Added support for DeepSeek V3 0324 model.
- Added a new `/context` command that automatically identifies which files need to be edited for a given request.
- Added `/edit` as an alias for the `/editor` command.
- Added "overeager" mode for Claude 3.7 Sonnet models to try and keep it working within the requested scope.
- Aider wrote 65% of the code in this release.
### Aider v0.78.0
- Added support for thinking tokens for OpenRouter Sonnet 3.7.
- Added commands to switch between model types: `/editor-model` for Editor Model, and `/weak-model` for Weak Model, by csala.
- Added model setting validation to ignore `--reasoning-effort` and `--thinking-tokens` if the model doesn't support them.
- Added `--check-model-accepts-settings` flag (default: true) to force unsupported model settings.
- Annotated which models support reasoning_effort and thinking_tokens settings in the model settings data.
- Improved code block rendering in markdown output with better padding using NoInsetMarkdown.
- Added `--git-commit-verify` flag (default: False) to control whether git commit hooks are bypassed.
- Fixed autocompletion for `/ask`, `/code`, and `/architect` commands, by shladnik.
- Added vi-like behavior when pressing enter in multiline-mode while in vi normal/navigation-mode, by Marco Mayer.
- Added AWS_PROFILE support for Bedrock models, allowing use of AWS profiles instead of explicit credentials, by lentil32.
- Enhanced `--aiderignore` argument to resolve both absolute and relative paths, by mopemope.
- Improved platform information handling to gracefully handle retrieval errors.
- Aider wrote 92% of the code in this release.
### Aider v0.77.1
- Bumped dependencies to pickup litellm fix for Ollama.
- Added support for `openrouter/google/gemma-3-27b-it` model.
- Updated exclude patterns for help documentation.
### Aider v0.77.0
- Big upgrade in [programming languages supported](https://aider.chat/docs/languages.html) by adopting [tree-sitter-language-pack](https://github.com/Goldziher/tree-sitter-language-pack/).
- 130 new languages with linter support.
- 20 new languages with repo-map support.
- Added `/think-tokens` command to set thinking token budget with support for human-readable formats (8k, 10.5k, 0.5M).
- Added `/reasoning-effort` command to control model reasoning level.
- The `/think-tokens` and `/reasoning-effort` commands display current settings when called without arguments.
- Display of thinking token budget and reasoning effort in model information.
- Changed `--thinking-tokens` argument to accept string values with human-readable formats.
- Added `--auto-accept-architect` flag (default: true) to automatically accept changes from architect coder format without confirmation.
- Added support for `cohere_chat/command-a-03-2025` and `gemini/gemma-3-27b-it`
- The bare `/drop` command now preserves original read-only files provided via args.read.
- Fixed a bug where default model would be set by deprecated `--shortcut` switches even when already specified in the command line.
- Improved AutoCompleter to require 3 characters for autocompletion to reduce noise.
- Aider wrote 72% of the code in this release.
### Aider v0.76.2
- Fixed handling of JSONDecodeError when loading model cache file.
- Fixed handling of GitCommandError when retrieving git user configuration.
- Aider wrote 75% of the code in this release.
### Aider v0.76.1
- Added ignore_permission_denied option to file watcher to prevent errors when accessing restricted files, by Yutaka Matsubara.
- Aider wrote 0% of the code in this release.
### Aider v0.76.0
- Improved support for thinking/reasoningmodels:
- Added `--thinking-tokens` CLI option to control token budget for models that support thinking.
- Display thinking/reasoning content from LLMs which return it.
- Enhanced handling of reasoning tags to better clean up model responses.
- Added deprecation warning for `remove_reasoning` setting, now replaced by `reasoning_tag`.
- Aider will notify you when it's completed the last request and needs your input:
- Added [notifications when LLM responses are ready](https://aider.chat/docs/usage/notifications.html) with `--notifications` flag.
- Specify desktop notification command with `--notifications-command`.
- Added support for QWQ 32B.
- Switch to `tree-sitter-language-pack` for tree sitter support.
- Improved error handling for EOF (Ctrl+D) in user input prompts.
- Added helper function to ensure hex color values have a # prefix.
- Fixed handling of Git errors when reading staged files.
- Improved SSL verification control for model information requests.
- Improved empty LLM response handling with clearer warning messages.
- Fixed Git identity retrieval to respect global configuration, by Akira Komamura.
- Offer to install dependencies for Bedrock and Vertex AI models.
- Deprecated model shortcut args (like --4o, --opus) in favor of the --model flag.
- Aider wrote 85% of the code in this release.
### Aider v0.75.3
- Support for V3 free on OpenRouter: `--model openrouter/deepseek/deepseek-chat:free`.
### Aider v0.75.2
- Added support for Claude 3.7 Sonnet models on OpenRouter, Bedrock and Vertex AI.
- Aider wrote 47% of the code in this release.
- Updated default model to Claude 3.7 Sonnet on OpenRouter.
- Added support for GPT-4.5-preview model.
- Added support for Claude 3.7 Sonnet:beta on OpenRouter.
- Fixed weak_model_name patterns to match main model name patterns for some models.
### Aider v0.75.1

251
README.md
View file

@ -1,144 +1,177 @@
<p align="center">
<a href="https://aider.chat/"><img src="https://aider.chat/assets/logo.svg" alt="Aider Logo" width="300"></a>
</p>
<!-- Edit README.md, not index.md -->
<h1 align="center">
AI Pair Programming in Your Terminal
</h1>
# Aider is AI pair programming in your terminal
Aider lets you pair program with LLMs,
to edit code in your local git repository.
Start a new project or work with an existing code base.
Aider works best with Claude 3.5 Sonnet, DeepSeek R1 & Chat V3, OpenAI o1, o3-mini & GPT-4o. Aider can [connect to almost any LLM, including local models](https://aider.chat/docs/llms.html).
<p align="center">
Aider lets you pair program with LLMs to start a new project or build on your existing codebase.
</p>
<!-- SCREENCAST START -->
<p align="center">
<img
src="https://aider.chat/assets/screencast.svg"
alt="aider screencast"
>
</p>
<!-- SCREENCAST END -->
<!-- VIDEO START
<p align="center">
<video style="max-width: 100%; height: auto;" autoplay loop muted playsinline>
<source src="/assets/shell-cmds-small.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</p>
VIDEO END -->
<p align="center">
<a href="https://discord.gg/Tv2uQnR88V">
<img src="https://img.shields.io/badge/Join-Discord-blue.svg"/>
</a>
<a href="https://aider.chat/docs/install.html">
<img src="https://img.shields.io/badge/Read-Docs-green.svg"/>
</a>
</p>
## Getting started
<!--[[[cog
# We can't "include" here.
# Because this page is rendered by GitHub as the repo README
cog.out(open("aider/website/_includes/get-started.md").read())
from scripts.homepage import get_badges_md
text = get_badges_md()
cog.out(text)
]]]-->
<a href="https://github.com/Aider-AI/aider/stargazers"><img alt="GitHub Stars" title="Total number of GitHub stars the Aider project has received"
src="https://img.shields.io/github/stars/Aider-AI/aider?style=flat-square&logo=github&color=f1c40f&labelColor=555555"/></a>
<a href="https://pypi.org/project/aider-chat/"><img alt="PyPI Downloads" title="Total number of installations via pip from PyPI"
src="https://img.shields.io/badge/📦%20Installs-2.0M-2ecc71?style=flat-square&labelColor=555555"/></a>
<img alt="Tokens per week" title="Number of tokens processed weekly by Aider users"
src="https://img.shields.io/badge/📈%20Tokens%2Fweek-15B-3498db?style=flat-square&labelColor=555555"/>
<a href="https://openrouter.ai/#options-menu"><img alt="OpenRouter Ranking" title="Aider's ranking among applications on the OpenRouter platform"
src="https://img.shields.io/badge/🏆%20OpenRouter-Top%2020-9b59b6?style=flat-square&labelColor=555555"/></a>
<a href="https://aider.chat/HISTORY.html"><img alt="Singularity" title="Percentage of the new code in Aider's last release written by Aider itself"
src="https://img.shields.io/badge/🔄%20Singularity-92%25-e74c3c?style=flat-square&labelColor=555555"/></a>
<!--[[[end]]]-->
</p>
If you already have python 3.8-3.13 installed, you can get started quickly like this:
## Features
### [Cloud and local LLMs](https://aider.chat/docs/llms.html)
<a href="https://aider.chat/docs/llms.html"><img src="https://aider.chat/assets/icons/brain.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Aider works best with Claude 3.7 Sonnet, DeepSeek R1 & Chat V3, OpenAI o1, o3-mini & GPT-4o, but can connect to almost any LLM, including local models.
<br>
### [Maps your codebase](https://aider.chat/docs/repomap.html)
<a href="https://aider.chat/docs/repomap.html"><img src="https://aider.chat/assets/icons/map-outline.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Aider makes a map of your entire codebase, which helps it work well in larger projects.
<br>
### [100+ code languages](https://aider.chat/docs/languages.html)
<a href="https://aider.chat/docs/languages.html"><img src="https://aider.chat/assets/icons/code-tags.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Aider works with most popular programming languages: python, javascript, rust, ruby, go, cpp, php, html, css, and dozens more.
<br>
### [Git integration](https://aider.chat/docs/git.html)
<a href="https://aider.chat/docs/git.html"><img src="https://aider.chat/assets/icons/source-branch.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Aider automatically commits changes with sensible commit messages. Use familiar git tools to easily diff, manage and undo AI changes.
<br>
### [Use in your IDE](https://aider.chat/docs/usage/watch.html)
<a href="https://aider.chat/docs/usage/watch.html"><img src="https://aider.chat/assets/icons/monitor.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Use aider from within your favorite IDE or editor. Ask for changes by adding comments to your code and aider will get to work.
<br>
### [Images & web pages](https://aider.chat/docs/usage/images-urls.html)
<a href="https://aider.chat/docs/usage/images-urls.html"><img src="https://aider.chat/assets/icons/image-multiple.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Add images and web pages to the chat to provide visual context, screenshots, reference docs, etc.
<br>
### [Voice-to-code](https://aider.chat/docs/usage/voice.html)
<a href="https://aider.chat/docs/usage/voice.html"><img src="https://aider.chat/assets/icons/microphone.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Speak with aider about your code! Request new features, test cases or bug fixes using your voice and let aider implement the changes.
<br>
### [Linting & testing](https://aider.chat/docs/usage/lint-test.html)
<a href="https://aider.chat/docs/usage/lint-test.html"><img src="https://aider.chat/assets/icons/check-all.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Automatically lint and test your code every time aider makes changes. Aider can fix problems detected by your linters and test suites.
<br>
### [Copy/paste to web chat](https://aider.chat/docs/usage/copypaste.html)
<a href="https://aider.chat/docs/usage/copypaste.html"><img src="https://aider.chat/assets/icons/content-copy.svg" width="32" height="32" align="left" valign="middle" style="margin-right:10px"></a>
Work with any LLM via its web chat interface. Aider streamlines copy/pasting code context and edits back and forth with a browser.
## Getting Started
```bash
python -m pip install aider-install
aider-install
# Change directory into your code base
# Change directory into your codebase
cd /to/your/project
# Work with DeepSeek via DeepSeek's API
aider --model deepseek --api-key deepseek=your-key-goes-here
# DeepSeek
aider --model deepseek --api-key deepseek=<key>
# Work with Claude 3.5 Sonnet via Anthropic's API
aider --model sonnet --api-key anthropic=your-key-goes-here
# Claude 3.7 Sonnet
aider --model sonnet --api-key anthropic=<key>
# Work with GPT-4o via OpenAI's API
aider --model gpt-4o --api-key openai=your-key-goes-here
# Work with Sonnet via OpenRouter's API
aider --model openrouter/anthropic/claude-3.5-sonnet --api-key openrouter=your-key-goes-here
# Work with DeepSeek via OpenRouter's API
aider --model openrouter/deepseek/deepseek-chat --api-key openrouter=your-key-goes-here
# o3-mini
aider --model o3-mini --api-key openai=<key>
```
<!--[[[end]]]-->
See the
[installation instructions](https://aider.chat/docs/install.html)
and
[usage documentation](https://aider.chat/docs/usage.html)
for more details.
See the [installation instructions](https://aider.chat/docs/install.html) and [usage documentation](https://aider.chat/docs/usage.html) for more details.
## Features
## More Information
- Run aider with the files you want to edit: `aider <file1> <file2> ...`
- Ask for changes:
- Add new features or test cases.
- Describe a bug.
- Paste in an error message or GitHub issue URL.
- Refactor code.
- Update docs.
- Aider will edit your files to complete your request.
- Aider [automatically git commits](https://aider.chat/docs/git.html) changes with a sensible commit message.
- [Use aider inside your favorite editor or IDE](https://aider.chat/docs/usage/watch.html).
- Aider works with [most popular languages](https://aider.chat/docs/languages.html): python, javascript, typescript, php, html, css, and more...
- Aider can edit multiple files at once for complex requests.
- Aider uses a [map of your entire git repo](https://aider.chat/docs/repomap.html), which helps it work well in larger codebases.
- Edit files in your editor or IDE while chatting with aider,
and it will always use the latest version.
Pair program with AI.
- [Add images to the chat](https://aider.chat/docs/usage/images-urls.html) (GPT-4o, Claude 3.5 Sonnet, etc).
- [Add URLs to the chat](https://aider.chat/docs/usage/images-urls.html) and aider will read their content.
- [Code with your voice](https://aider.chat/docs/usage/voice.html).
- Aider works best with Claude 3.5 Sonnet, DeepSeek V3, o1 & GPT-4o and can [connect to almost any LLM](https://aider.chat/docs/llms.html).
## Top tier performance
[Aider has one of the top scores on SWE Bench](https://aider.chat/2024/06/02/main-swe-bench.html).
SWE Bench is a challenging software engineering benchmark where aider
solved *real* GitHub issues from popular open source
projects like django, scikitlearn, matplotlib, etc.
## More info
- [Documentation](https://aider.chat/)
- [Installation](https://aider.chat/docs/install.html)
- [Usage](https://aider.chat/docs/usage.html)
- [Tutorial videos](https://aider.chat/docs/usage/tutorials.html)
### Documentation
- [Installation Guide](https://aider.chat/docs/install.html)
- [Usage Guide](https://aider.chat/docs/usage.html)
- [Tutorial Videos](https://aider.chat/docs/usage/tutorials.html)
- [Connecting to LLMs](https://aider.chat/docs/llms.html)
- [Configuration](https://aider.chat/docs/config.html)
- [Configuration Options](https://aider.chat/docs/config.html)
- [Troubleshooting](https://aider.chat/docs/troubleshooting.html)
- [FAQ](https://aider.chat/docs/faq.html)
### Community & Resources
- [LLM Leaderboards](https://aider.chat/docs/leaderboards/)
- [GitHub](https://github.com/Aider-AI/aider)
- [Discord](https://discord.gg/Tv2uQnR88V)
- [GitHub Repository](https://github.com/Aider-AI/aider)
- [Discord Community](https://discord.gg/Tv2uQnR88V)
- [Blog](https://aider.chat/blog/)
## Kind Words From Users
## Kind words from users
- *"My life has changed... There's finally an AI coding tool that's good enough to keep up with me... Aider... It's going to rock your world."* — [Eric S. Raymond](https://x.com/esrtweet/status/1910809356381413593)
- *"The best free open source AI coding assistant."* — [IndyDevDan](https://youtu.be/YALpX8oOn78)
- *"The best AI coding assistant so far."* — [Matthew Berman](https://www.youtube.com/watch?v=df8afeb1FY8)
- *"Aider ... has easily quadrupled my coding productivity."* — [SOLAR_FIELDS](https://news.ycombinator.com/item?id=36212100)
- *"It's a cool workflow... Aider's ergonomics are perfect for me."* — [qup](https://news.ycombinator.com/item?id=38185326)
- *"It's really like having your senior developer live right in your Git repo - truly amazing!"* — [rappster](https://github.com/Aider-AI/aider/issues/124)
- *"What an amazing tool. It's incredible."* — [valyagolev](https://github.com/Aider-AI/aider/issues/6#issue-1722897858)
- *"Aider is such an astounding thing!"* — [cgrothaus](https://github.com/Aider-AI/aider/issues/82#issuecomment-1631876700)
- *"It was WAY faster than I would be getting off the ground and making the first few working versions."* — [Daniel Feldman](https://twitter.com/d_feldman/status/1662295077387923456)
- *"THANK YOU for Aider! It really feels like a glimpse into the future of coding."* — [derwiki](https://news.ycombinator.com/item?id=38205643)
- *"It's just amazing. It is freeing me to do things I felt were out my comfort zone before."* — [Dougie](https://discord.com/channels/1131200896827654144/1174002618058678323/1174084556257775656)
- *"This project is stellar."* — [funkytaco](https://github.com/Aider-AI/aider/issues/112#issuecomment-1637429008)
- *"Amazing project, definitely the best AI coding assistant I've used."* — [joshuavial](https://github.com/Aider-AI/aider/issues/84)
- *"I absolutely love using Aider ... It makes software development feel so much lighter as an experience."* — [principalideal0](https://discord.com/channels/1131200896827654144/1133421607499595858/1229689636012691468)
- *"I have been recovering from multiple shoulder surgeries ... and have used aider extensively. It has allowed me to continue productivity."* — [codeninja](https://www.reddit.com/r/OpenAI/s/nmNwkHy1zG)
- *"I am an aider addict. I'm getting so much more work done, but in less time."* — [dandandan](https://discord.com/channels/1131200896827654144/1131200896827654149/1135913253483069470)
- *"After wasting $100 on tokens trying to find something better, I'm back to Aider. It blows everything else out of the water hands down, there's no competition whatsoever."* — [SystemSculpt](https://discord.com/channels/1131200896827654144/1131200896827654149/1178736602797846548)
- *"Aider is amazing, coupled with Sonnet 3.5 it's quite mind blowing."* — [Josh Dingus](https://discord.com/channels/1131200896827654144/1133060684540813372/1262374225298198548)
- *"Hands down, this is the best AI coding assistant tool so far."* — [IndyDevDan](https://www.youtube.com/watch?v=MPYFPvxfGZs)
- *"[Aider] changed my daily coding workflows. It's mind-blowing how a single Python application can change your life."* — [maledorak](https://discord.com/channels/1131200896827654144/1131200896827654149/1258453375620747264)
- *"Best agent for actual dev work in existing codebases."* — [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20)
- *"One of my favorite pieces of software. Blazing trails on new paradigms!"* — [Chris Wall](https://x.com/chris65536/status/1905053299251798432)
- *"Aider has been revolutionary for me and my work."* — [Starry Hope](https://x.com/starryhopeblog/status/1904985812137132056)
- *"Try aider! One of the best ways to vibe code."* — [Chris Wall](https://x.com/Chris65536/status/1905053418961391929)
- *"Aider is hands down the best. And it's free and opensource."* — [AriyaSavakaLurker](https://www.reddit.com/r/ChatGPTCoding/comments/1ik16y6/whats_your_take_on_aider/mbip39n/)
- *"Aider is also my best friend."* — [jzn21](https://www.reddit.com/r/ChatGPTCoding/comments/1heuvuo/aider_vs_cline_vs_windsurf_vs_cursor/m27dcnb/)
- *"Try Aider, it's worth it."* — [jorgejhms](https://www.reddit.com/r/ChatGPTCoding/comments/1heuvuo/aider_vs_cline_vs_windsurf_vs_cursor/m27cp99/)
- *"I like aider :)"* — [Chenwei Cui](https://x.com/ccui42/status/1904965344999145698)
- *"Aider is the precision tool of LLM code gen... Minimal, thoughtful and capable of surgical changes to your codebase all while keeping the developer in control."* — [Reilly Sweetland](https://x.com/rsweetland/status/1904963807237259586)
- *"Cannot believe aider vibe coded a 650 LOC feature across service and cli today in 1 shot."* - [autopoietist](https://discord.com/channels/1131200896827654144/1131200896827654149/1355675042259796101)
- *"Oh no the secret is out! Yes, Aider is the best coding tool around. I highly, highly recommend it to anyone."* — [Joshua D Vander Hook](https://x.com/jodavaho/status/1911154899057795218)
- *"thanks to aider, i have started and finished three personal projects within the last two days"* — [joseph stalzyn](https://x.com/anitaheeder/status/1908338609645904160)
- *"Been using aider as my daily driver for over a year ... I absolutely love the tool, like beyond words."* — [koleok](https://discord.com/channels/1131200896827654144/1273248471394291754/1356727448372252783)
- *"aider is really cool"* — [kache (@yacineMTB)](https://x.com/yacineMTB/status/1911224442430124387)
- *The best free open source AI coding assistant.* -- [IndyDevDan](https://youtu.be/YALpX8oOn78)
- *The best AI coding assistant so far.* -- [Matthew Berman](https://www.youtube.com/watch?v=df8afeb1FY8)
- *Aider ... has easily quadrupled my coding productivity.* -- [SOLAR_FIELDS](https://news.ycombinator.com/item?id=36212100)
- *It's a cool workflow... Aider's ergonomics are perfect for me.* -- [qup](https://news.ycombinator.com/item?id=38185326)
- *It's really like having your senior developer live right in your Git repo - truly amazing!* -- [rappster](https://github.com/Aider-AI/aider/issues/124)
- *What an amazing tool. It's incredible.* -- [valyagolev](https://github.com/Aider-AI/aider/issues/6#issue-1722897858)
- *Aider is such an astounding thing!* -- [cgrothaus](https://github.com/Aider-AI/aider/issues/82#issuecomment-1631876700)
- *It was WAY faster than I would be getting off the ground and making the first few working versions.* -- [Daniel Feldman](https://twitter.com/d_feldman/status/1662295077387923456)
- *THANK YOU for Aider! It really feels like a glimpse into the future of coding.* -- [derwiki](https://news.ycombinator.com/item?id=38205643)
- *It's just amazing. It is freeing me to do things I felt were out my comfort zone before.* -- [Dougie](https://discord.com/channels/1131200896827654144/1174002618058678323/1174084556257775656)
- *This project is stellar.* -- [funkytaco](https://github.com/Aider-AI/aider/issues/112#issuecomment-1637429008)
- *Amazing project, definitely the best AI coding assistant I've used.* -- [joshuavial](https://github.com/Aider-AI/aider/issues/84)
- *I absolutely love using Aider ... It makes software development feel so much lighter as an experience.* -- [principalideal0](https://discord.com/channels/1131200896827654144/1133421607499595858/1229689636012691468)
- *I have been recovering from multiple shoulder surgeries ... and have used aider extensively. It has allowed me to continue productivity.* -- [codeninja](https://www.reddit.com/r/OpenAI/s/nmNwkHy1zG)
- *I am an aider addict. I'm getting so much more work done, but in less time.* -- [dandandan](https://discord.com/channels/1131200896827654144/1131200896827654149/1135913253483069470)
- *After wasting $100 on tokens trying to find something better, I'm back to Aider. It blows everything else out of the water hands down, there's no competition whatsoever.* -- [SystemSculpt](https://discord.com/channels/1131200896827654144/1131200896827654149/1178736602797846548)
- *Aider is amazing, coupled with Sonnet 3.5 its quite mind blowing.* -- [Josh Dingus](https://discord.com/channels/1131200896827654144/1133060684540813372/1262374225298198548)
- *Hands down, this is the best AI coding assistant tool so far.* -- [IndyDevDan](https://www.youtube.com/watch?v=MPYFPvxfGZs)
- *[Aider] changed my daily coding workflows. It's mind-blowing how a single Python application can change your life.* -- [maledorak](https://discord.com/channels/1131200896827654144/1131200896827654149/1258453375620747264)
- *Best agent for actual dev work in existing codebases.* -- [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20)

View file

@ -1,6 +1,6 @@
from packaging import version
__version__ = "0.75.2.dev"
__version__ = "0.82.3.dev"
safe_version = __version__
try:

View file

@ -3,6 +3,7 @@
import argparse
import os
import sys
from pathlib import Path
import configargparse
@ -12,10 +13,20 @@ from aider.args_formatter import (
MarkdownHelpFormatter,
YamlHelpFormatter,
)
from aider.deprecated import add_deprecated_model_args
from .dump import dump # noqa: F401
def resolve_aiderignore_path(path_str, git_root=None):
path = Path(path_str)
if path.is_absolute():
return str(path)
elif git_root:
return str(Path(git_root) / path)
return str(path)
def default_env_file(git_root):
return os.path.join(git_root, ".env") if git_root else ".env"
@ -38,98 +49,6 @@ def get_parser(default_config_files, git_root):
default=None,
help="Specify the model to use for the main chat",
)
opus_model = "claude-3-opus-20240229"
group.add_argument(
"--opus",
action="store_const",
dest="model",
const=opus_model,
help=f"Use {opus_model} model for the main chat",
)
sonnet_model = "anthropic/claude-3-7-sonnet-20250219"
group.add_argument(
"--sonnet",
action="store_const",
dest="model",
const=sonnet_model,
help=f"Use {sonnet_model} model for the main chat",
)
haiku_model = "claude-3-5-haiku-20241022"
group.add_argument(
"--haiku",
action="store_const",
dest="model",
const=haiku_model,
help=f"Use {haiku_model} model for the main chat",
)
gpt_4_model = "gpt-4-0613"
group.add_argument(
"--4",
"-4",
action="store_const",
dest="model",
const=gpt_4_model,
help=f"Use {gpt_4_model} model for the main chat",
)
gpt_4o_model = "gpt-4o"
group.add_argument(
"--4o",
action="store_const",
dest="model",
const=gpt_4o_model,
help=f"Use {gpt_4o_model} model for the main chat",
)
gpt_4o_mini_model = "gpt-4o-mini"
group.add_argument(
"--mini",
action="store_const",
dest="model",
const=gpt_4o_mini_model,
help=f"Use {gpt_4o_mini_model} model for the main chat",
)
gpt_4_turbo_model = "gpt-4-1106-preview"
group.add_argument(
"--4-turbo",
action="store_const",
dest="model",
const=gpt_4_turbo_model,
help=f"Use {gpt_4_turbo_model} model for the main chat",
)
gpt_3_model_name = "gpt-3.5-turbo"
group.add_argument(
"--35turbo",
"--35-turbo",
"--3",
"-3",
action="store_const",
dest="model",
const=gpt_3_model_name,
help=f"Use {gpt_3_model_name} model for the main chat",
)
deepseek_model = "deepseek/deepseek-chat"
group.add_argument(
"--deepseek",
action="store_const",
dest="model",
const=deepseek_model,
help=f"Use {deepseek_model} model for the main chat",
)
o1_mini_model = "o1-mini"
group.add_argument(
"--o1-mini",
action="store_const",
dest="model",
const=o1_mini_model,
help=f"Use {o1_mini_model} model for the main chat",
)
o1_preview_model = "o1-preview"
group.add_argument(
"--o1-preview",
action="store_const",
dest="model",
const=o1_preview_model,
help=f"Use {o1_preview_model} model for the main chat",
)
##########
group = parser.add_argument_group("API Keys and settings")
@ -208,6 +127,11 @@ def get_parser(default_config_files, git_root):
type=str,
help="Set the reasoning_effort API parameter (default: not set)",
)
group.add_argument(
"--thinking-tokens",
type=str,
help="Set the thinking token budget for models that support it (default: not set)",
)
group.add_argument(
"--verify-ssl",
action=argparse.BooleanOptionalAction,
@ -234,6 +158,12 @@ def get_parser(default_config_files, git_root):
const="architect",
help="Use architect edit format for the main chat",
)
group.add_argument(
"--auto-accept-architect",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable automatic acceptance of architect changes (default: True)",
)
group.add_argument(
"--weak-model",
metavar="WEAK_MODEL",
@ -261,6 +191,14 @@ def get_parser(default_config_files, git_root):
default=True,
help="Only work with models that have meta-data available (default: True)",
)
group.add_argument(
"--check-model-accepts-settings",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Check if model accepts settings like reasoning_effort/thinking_tokens (default: True)"
),
)
group.add_argument(
"--max-chat-history-tokens",
type=int,
@ -460,9 +398,11 @@ def get_parser(default_config_files, git_root):
default_aiderignore_file = (
os.path.join(git_root, ".aiderignore") if git_root else ".aiderignore"
)
group.add_argument(
"--aiderignore",
metavar="AIDERIGNORE",
type=lambda path_str: resolve_aiderignore_path(path_str, git_root),
default=default_aiderignore_file,
help="Specify the aider ignore file (default: .aiderignore in git root)",
)
@ -508,6 +448,12 @@ def get_parser(default_config_files, git_root):
default=False,
help="Prefix all commit messages with 'aider: ' (default: False)",
)
group.add_argument(
"--git-commit-verify",
action=argparse.BooleanOptionalAction,
default=False,
help="Enable/disable git pre-commit hooks with --no-verify (default: False)",
)
group.add_argument(
"--commit",
action="store_true",
@ -813,6 +759,24 @@ def get_parser(default_config_files, git_root):
default=False,
help="Enable/disable multi-line input mode with Meta-Enter to submit (default: False)",
)
group.add_argument(
"--notifications",
action=argparse.BooleanOptionalAction,
default=False,
help=(
"Enable/disable terminal bell notifications when LLM responses are ready (default:"
" False)"
),
)
group.add_argument(
"--notifications-command",
metavar="COMMAND",
default=None,
help=(
"Specify a command to run for notifications instead of the terminal bell. If not"
" specified, a default command for your OS may be used."
),
)
group.add_argument(
"--detect-urls",
action=argparse.BooleanOptionalAction,
@ -823,12 +787,11 @@ def get_parser(default_config_files, git_root):
"--editor",
help="Specify which editor to use for the /editor command",
)
group.add_argument(
"--install-tree-sitter-language-pack",
action="store_true",
help="Install the tree_sitter_language_pack (experimental)",
default=False,
)
##########
group = parser.add_argument_group("Deprecated model settings")
# Add deprecated model shortcut arguments
add_deprecated_model_args(parser, group)
return parser

View file

@ -143,16 +143,22 @@ class YamlHelpFormatter(argparse.HelpFormatter):
default = "true"
if default:
parts.append(f"#{switch}: {default}\n")
if "#" in default:
parts.append(f'#{switch}: "{default}"\n')
else:
parts.append(f"#{switch}: {default}\n")
elif action.nargs in ("*", "+") or isinstance(action, argparse._AppendAction):
parts.append(f"#{switch}: xxx")
parts.append("## Specify multiple values like this:")
parts.append(f"#{switch}:")
parts.append(f"# - xxx")
parts.append(f"# - yyy")
parts.append(f"# - zzz")
parts.append("# - xxx")
parts.append("# - yyy")
parts.append("# - zzz")
else:
parts.append(f"#{switch}: xxx\n")
if switch.endswith("color"):
parts.append(f'#{switch}: "xxx"\n')
else:
parts.append(f"#{switch}: xxx\n")
###
# parts.append(str(action))

View file

@ -1,12 +1,16 @@
from .architect_coder import ArchitectCoder
from .ask_coder import AskCoder
from .base_coder import Coder
from .context_coder import ContextCoder
from .editblock_coder import EditBlockCoder
from .editblock_fenced_coder import EditBlockFencedCoder
from .editor_diff_fenced_coder import EditorDiffFencedCoder
from .editor_editblock_coder import EditorEditBlockCoder
from .editor_whole_coder import EditorWholeFileCoder
from .help_coder import HelpCoder
from .patch_coder import PatchCoder
from .udiff_coder import UnifiedDiffCoder
from .udiff_simple import UnifiedDiffSimpleCoder
from .wholefile_coder import WholeFileCoder
from .batch_coder import BatchCoder
# from .single_wholefile_func_coder import SingleWholeFileFunctionCoder
@ -19,9 +23,13 @@ __all__ = [
EditBlockFencedCoder,
BatchCoder,
WholeFileCoder,
PatchCoder,
UnifiedDiffCoder,
UnifiedDiffSimpleCoder,
# SingleWholeFileFunctionCoder,
ArchitectCoder,
EditorEditBlockCoder,
EditorWholeFileCoder,
EditorDiffFencedCoder,
ContextCoder,
]

View file

@ -6,6 +6,7 @@ from .base_coder import Coder
class ArchitectCoder(AskCoder):
edit_format = "architect"
gpt_prompts = ArchitectPrompts()
auto_accept_architect = False
def reply_completed(self):
content = self.partial_response_content
@ -13,7 +14,7 @@ class ArchitectCoder(AskCoder):
if not content or not content.strip():
return
if not self.io.confirm_ask("Edit the files?"):
if not self.auto_accept_architect and not self.io.confirm_ask("Edit the files?"):
return
kwargs = dict()

View file

@ -28,6 +28,12 @@ from aider.io import ConfirmGroup, InputOutput
from aider.linter import Linter
from aider.llm import litellm
from aider.models import RETRY_TIMEOUT
from aider.reasoning_tags import (
REASONING_TAG,
format_reasoning_content,
remove_reasoning_content,
replace_reasoning_tags,
)
from aider.repo import ANY_GIT_ERROR, GitRepo
from aider.repomap import RepoMap
from aider.run_cmd import run_cmd
@ -201,10 +207,22 @@ class Coder:
prefix = "Model"
output = f"{prefix}: {main_model.name} with {self.edit_format} edit format"
# Check for thinking token budget
thinking_tokens = main_model.get_thinking_tokens()
if thinking_tokens:
output += f", {thinking_tokens} think tokens"
# Check for reasoning effort
reasoning_effort = main_model.get_reasoning_effort()
if reasoning_effort:
output += f", reasoning {reasoning_effort}"
if self.add_cache_headers or main_model.caches_by_default:
output += ", prompt cache"
if main_model.info.get("supports_assistant_prefill"):
output += ", infinite output"
lines.append(output)
if self.edit_format == "architect":
@ -304,6 +322,7 @@ class Coder:
ignore_mentions=None,
file_watcher=None,
auto_copy_context=False,
auto_accept_architect=True,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
self.analytics = analytics if analytics is not None else Analytics()
@ -316,6 +335,7 @@ class Coder:
self.abs_root_path_cache = {}
self.auto_copy_context = auto_copy_context
self.auto_accept_architect = auto_accept_architect
self.ignore_mentions = ignore_mentions
if not self.ignore_mentions:
@ -375,6 +395,10 @@ class Coder:
self.pretty = self.io.pretty
self.main_model = main_model
# Set the reasoning tag name based on model settings or default
self.reasoning_tag_name = (
self.main_model.reasoning_tag if self.main_model.reasoning_tag else REASONING_TAG
)
self.stream = stream and main_model.streaming
@ -898,10 +922,11 @@ class Coder:
else:
self.io.tool_error(text)
url_pattern = re.compile(r"(https?://[^\s/$.?#].[^\s]*)")
# Exclude double quotes from the matched URL characters
url_pattern = re.compile(r'(https?://[^\s/$.?#].[^\s"]*)')
urls = list(set(url_pattern.findall(text))) # Use set to remove duplicates
for url in urls:
url = url.rstrip(".',\"")
url = url.rstrip(".',\"}") # Added } to the characters to strip
self.io.offer_url(url)
return urls
@ -910,7 +935,8 @@ class Coder:
if not self.detect_urls:
return inp
url_pattern = re.compile(r"(https?://[^\s/$.?#].[^\s]*[^\s,.])")
# Exclude double quotes from the matched URL characters
url_pattern = re.compile(r'(https?://[^\s/$.?#].[^\s"]*[^\s,.])')
urls = list(set(url_pattern.findall(inp))) # Use set to remove duplicates
group = ConfirmGroup(urls)
for url in urls:
@ -1006,7 +1032,13 @@ class Coder:
return None
def get_platform_info(self):
platform_text = f"- Platform: {platform.platform()}\n"
platform_text = ""
try:
platform_text = f"- Platform: {platform.platform()}\n"
except KeyError:
# Skip platform info if it can't be retrieved
platform_text = "- Platform information unavailable\n"
shell_var = "COMSPEC" if os.name == "nt" else "SHELL"
shell_val = os.getenv(shell_var)
platform_text += f"- Shell: {shell_var}={shell_val}\n"
@ -1047,17 +1079,25 @@ class Coder:
return platform_text
def fmt_system_prompt(self, prompt):
lazy_prompt = self.gpt_prompts.lazy_prompt if self.main_model.lazy else ""
if self.main_model.lazy:
lazy_prompt = self.gpt_prompts.lazy_prompt
elif self.main_model.overeager:
lazy_prompt = self.gpt_prompts.overeager_prompt
else:
lazy_prompt = ""
platform_text = self.get_platform_info()
if self.suggest_shell_commands:
shell_cmd_prompt = self.gpt_prompts.shell_cmd_prompt.format(platform=platform_text)
shell_cmd_reminder = self.gpt_prompts.shell_cmd_reminder.format(platform=platform_text)
rename_with_shell = self.gpt_prompts.rename_with_shell
else:
shell_cmd_prompt = self.gpt_prompts.no_shell_cmd_prompt.format(platform=platform_text)
shell_cmd_reminder = self.gpt_prompts.no_shell_cmd_reminder.format(
platform=platform_text
)
rename_with_shell = ""
if self.chat_language:
language = self.chat_language
@ -1077,7 +1117,9 @@ class Coder:
lazy_prompt=lazy_prompt,
platform=platform_text,
shell_cmd_prompt=shell_cmd_prompt,
rename_with_shell=rename_with_shell,
shell_cmd_reminder=shell_cmd_reminder,
go_ahead_tip=self.gpt_prompts.go_ahead_tip,
language=language,
)
@ -1280,6 +1322,9 @@ class Coder:
def send_message(self, inp):
self.event("message_send_starting")
# Notify IO that LLM processing is starting
self.io.llm_started()
self.cur_messages += [
dict(role="user", content=inp),
]
@ -1369,11 +1414,14 @@ class Coder:
self.mdstream = None
self.partial_response_content = self.get_multi_response_content_in_progress(True)
self.partial_response_content = self.main_model.remove_reasoning_content(
self.partial_response_content
)
self.remove_reasoning_content()
self.multi_response_content = ""
###
# print()
# print("=" * 20)
# dump(self.partial_response_content)
self.io.tool_output()
self.show_usage_report()
@ -1414,7 +1462,8 @@ class Coder:
return
try:
self.reply_completed()
if self.reply_completed():
return
except KeyboardInterrupt:
interrupted = True
@ -1557,30 +1606,30 @@ class Coder:
)
]
def get_file_mentions(self, content):
def get_file_mentions(self, content, ignore_current=False):
words = set(word for word in content.split())
# drop sentence punctuation from the end
words = set(word.rstrip(",.!;:?") for word in words)
# strip away all kinds of quotes
quotes = "".join(['"', "'", "`"])
quotes = "\"'`*_"
words = set(word.strip(quotes) for word in words)
addable_rel_fnames = self.get_addable_relative_files()
if ignore_current:
addable_rel_fnames = self.get_all_relative_files()
existing_basenames = {}
else:
addable_rel_fnames = self.get_addable_relative_files()
# Get basenames of files already in chat or read-only
existing_basenames = {os.path.basename(f) for f in self.get_inchat_relative_files()} | {
os.path.basename(self.get_rel_fname(f)) for f in self.abs_read_only_fnames
}
# Get basenames of files already in chat or read-only
existing_basenames = {os.path.basename(f) for f in self.get_inchat_relative_files()} | {
os.path.basename(self.get_rel_fname(f)) for f in self.abs_read_only_fnames
}
mentioned_rel_fnames = set()
fname_to_rel_fnames = {}
for rel_fname in addable_rel_fnames:
# Skip files that share a basename with files already in chat
if os.path.basename(rel_fname) in existing_basenames:
continue
normalized_rel_fname = rel_fname.replace("\\", "/")
normalized_words = set(word.replace("\\", "/") for word in words)
if normalized_rel_fname in normalized_words:
@ -1595,6 +1644,10 @@ class Coder:
fname_to_rel_fnames[fname].append(rel_fname)
for fname, rel_fnames in fname_to_rel_fnames.items():
# If the basename is already in chat, don't add based on a basename mention
if fname in existing_basenames:
continue
# If the basename mention is unique among addable files and present in the text
if len(rel_fnames) == 1 and fname in words:
mentioned_rel_fnames.add(rel_fnames[0])
@ -1623,6 +1676,9 @@ class Coder:
return prompts.added_files.format(fnames=", ".join(added_fnames))
def send(self, messages, model=None, functions=None):
self.got_reasoning_content = False
self.ended_reasoning_content = False
if not model:
model = self.main_model
@ -1690,6 +1746,14 @@ class Coder:
except AttributeError as func_err:
show_func_err = func_err
try:
reasoning_content = completion.choices[0].message.reasoning_content
except AttributeError:
try:
reasoning_content = completion.choices[0].message.reasoning
except AttributeError:
reasoning_content = None
try:
self.partial_response_content = completion.choices[0].message.content or ""
except AttributeError as content_err:
@ -1708,6 +1772,15 @@ class Coder:
raise Exception("No data found in LLM response!")
show_resp = self.render_incremental_response(True)
if reasoning_content:
formatted_reasoning = format_reasoning_content(
reasoning_content, self.reasoning_tag_name
)
show_resp = formatted_reasoning + show_resp
show_resp = replace_reasoning_tags(show_resp, self.reasoning_tag_name)
self.io.assistant_output(show_resp, pretty=self.show_pretty())
if (
@ -1717,6 +1790,8 @@ class Coder:
raise FinishReasonLength()
def show_send_output_stream(self, completion):
received_content = False
for chunk in completion:
if len(chunk.choices) == 0:
continue
@ -1735,19 +1810,46 @@ class Coder:
self.partial_response_function_call[k] += v
else:
self.partial_response_function_call[k] = v
received_content = True
except AttributeError:
pass
text = ""
try:
text = chunk.choices[0].delta.content
if text:
self.partial_response_content += text
reasoning_content = chunk.choices[0].delta.reasoning_content
except AttributeError:
text = None
try:
reasoning_content = chunk.choices[0].delta.reasoning
except AttributeError:
reasoning_content = None
if reasoning_content:
if not self.got_reasoning_content:
text += f"<{REASONING_TAG}>\n\n"
text += reasoning_content
self.got_reasoning_content = True
received_content = True
try:
content = chunk.choices[0].delta.content
if content:
if self.got_reasoning_content and not self.ended_reasoning_content:
text += f"\n\n</{self.reasoning_tag_name}>\n\n"
self.ended_reasoning_content = True
text += content
received_content = True
except AttributeError:
pass
self.partial_response_content += text
if self.show_pretty():
self.live_incremental_response(False)
elif text:
# Apply reasoning tag formatting
text = replace_reasoning_tags(text, self.reasoning_tag_name)
try:
sys.stdout.write(text)
except UnicodeEncodeError:
@ -1759,13 +1861,26 @@ class Coder:
sys.stdout.flush()
yield text
if not received_content:
self.io.tool_warning("Empty response received from LLM. Check your provider account?")
def live_incremental_response(self, final):
show_resp = self.render_incremental_response(final)
# Apply any reasoning tag formatting
show_resp = replace_reasoning_tags(show_resp, self.reasoning_tag_name)
self.mdstream.update(show_resp, final=final)
def render_incremental_response(self, final):
return self.get_multi_response_content_in_progress()
def remove_reasoning_content(self):
"""Remove reasoning content from the model's response."""
self.partial_response_content = remove_reasoning_content(
self.partial_response_content,
self.reasoning_tag_name,
)
def calculate_and_show_tokens_and_cost(self, messages, completion=None):
prompt_tokens = 0
completion_tokens = 0
@ -1852,11 +1967,6 @@ class Coder:
f" ${format_cost(self.total_cost)} session."
)
if self.add_cache_headers and self.stream:
warning = " Use --no-stream for accurate caching costs."
self.usage_report = tokens_report + "\n" + cost_report + warning
return
if cache_hit_tokens and cache_write_tokens:
sep = "\n"
else:

View file

@ -14,6 +14,9 @@ You NEVER leave comments describing code without implementing it!
You always COMPLETELY IMPLEMENT the needed code!
"""
overeager_prompt = """Pay careful attention to the scope of the user's request.
Do what they ask, but no more."""
example_messages = []
files_content_prefix = """I have *added these files to the chat* so you can go ahead and edit them.
@ -50,3 +53,6 @@ Do not edit these files!
shell_cmd_reminder = ""
no_shell_cmd_prompt = ""
no_shell_cmd_reminder = ""
rename_with_shell = ""
go_ahead_tip = ""

View file

@ -0,0 +1,53 @@
from .base_coder import Coder
from .context_prompts import ContextPrompts
class ContextCoder(Coder):
"""Identify which files need to be edited for a given request."""
edit_format = "context"
gpt_prompts = ContextPrompts()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.repo_map:
return
self.repo_map.refresh = "always"
self.repo_map.max_map_tokens *= self.repo_map.map_mul_no_files
self.repo_map.map_mul_no_files = 1.0
def reply_completed(self):
content = self.partial_response_content
if not content or not content.strip():
return True
# dump(repr(content))
current_rel_fnames = set(self.get_inchat_relative_files())
mentioned_rel_fnames = set(self.get_file_mentions(content, ignore_current=True))
# dump(current_rel_fnames)
# dump(mentioned_rel_fnames)
# dump(current_rel_fnames == mentioned_rel_fnames)
if mentioned_rel_fnames == current_rel_fnames:
return True
if self.num_reflections >= self.max_reflections - 1:
return True
self.abs_fnames = set()
for fname in mentioned_rel_fnames:
self.add_rel_fname(fname)
# dump(self.get_inchat_relative_files())
self.reflected_message = self.gpt_prompts.try_again
# mentioned_idents = self.get_ident_mentions(cur_msg_text)
# if mentioned_idents:
return True
def check_for_file_mentions(self, content):
pass

View file

@ -0,0 +1,75 @@
# flake8: noqa: E501
from .base_prompts import CoderPrompts
class ContextPrompts(CoderPrompts):
main_system = """Act as an expert code analyst.
Understand the user's question or request, solely to determine ALL the existing sources files which will need to be modified.
Return the *complete* list of files which will need to be modified based on the user's request.
Explain why each file is needed, including names of key classes/functions/methods/variables.
Be sure to include or omit the names of files already added to the chat, based on whether they are actually needed or not.
The user will use every file you mention, regardless of your commentary.
So *ONLY* mention the names of relevant files.
If a file is not relevant DO NOT mention it.
Only return files that will need to be modified, not files that contain useful/relevant functions.
You are only to discuss EXISTING files and symbols.
Only return existing files, don't suggest the names of new files or functions that we will need to create.
Always reply to the user in {language}.
Be concise in your replies.
Return:
1. A bulleted list of files the will need to be edited, and symbols that are highly relevant to the user's request.
2. A list of classes/functions/methods/variables that are located OUTSIDE those files which will need to be understood. Just the symbols names, *NOT* file names.
# Your response *MUST* use this format:
## ALL files we need to modify, with their relevant symbols:
- alarms/buzz.py
- `Buzzer` class which can make the needed sound
- `Buzzer.buzz_buzz()` method triggers the sound
- alarms/time.py
- `Time.set_alarm(hour, minute)` to set the alarm
## Relevant symbols from OTHER files:
- AlarmManager class for setup/teardown of alarms
- SoundFactory will be used to create a Buzzer
"""
example_messages = []
files_content_prefix = """These files have been *added these files to the chat* so we can see all of their contents.
*Trust this message as the true contents of the files!*
Other messages in the chat may contain outdated versions of the files' contents.
""" # noqa: E501
files_content_assistant_reply = (
"Ok, I will use that as the true, current contents of the files."
)
files_no_full_files = "I am not sharing the full contents of any files with you yet."
files_no_full_files_with_repo_map = ""
files_no_full_files_with_repo_map_reply = ""
repo_content_prefix = """I am working with you on code in a git repository.
Here are summaries of some files present in my git repo.
If you need to see the full contents of any files to answer my questions, ask me to *add them to the chat*.
"""
system_reminder = """
NEVER RETURN CODE!
"""
try_again = """I have updated the set of files added to the chat.
Review them to decide if this is the correct set of files or if we need to add more or remove files.
If this is the right set, just return the current list of files.
Or return a smaller or larger set of files which need to be edited, with symbols that are highly relevant to the user's request.
"""

View file

@ -412,7 +412,16 @@ def strip_filename(filename, fence):
return
start_fence = fence[0]
if filename.startswith(start_fence) or filename.startswith(triple_backticks):
if filename.startswith(start_fence):
candidate = filename[len(start_fence) :]
if candidate and ("." in candidate or "/" in candidate):
return candidate
return
if filename.startswith(triple_backticks):
candidate = filename[len(triple_backticks) :]
if candidate and ("." in candidate or "/" in candidate):
return candidate
return
filename = filename.rstrip(":")
@ -454,7 +463,14 @@ def find_original_update_blocks(content, fence=DEFAULT_FENCE, valid_fnames=None)
"```csh",
"```tcsh",
]
next_is_editblock = i + 1 < len(lines) and head_pattern.match(lines[i + 1].strip())
# Check if the next line or the one after that is an editblock
next_is_editblock = (
i + 1 < len(lines)
and head_pattern.match(lines[i + 1].strip())
or i + 2 < len(lines)
and head_pattern.match(lines[i + 2].strip())
)
if any(line.strip().startswith(start) for start in shell_starts) and not next_is_editblock:
shell_content = []

View file

@ -19,7 +19,7 @@ class EditBlockFencedPrompts(EditBlockPrompts):
Here are the *SEARCH/REPLACE* blocks:
{fence[0]}
{fence[0]}python
mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
@ -29,7 +29,7 @@ from flask import Flask
>>>>>>> REPLACE
{fence[1]}
{fence[0]}
{fence[0]}python
mathweb/flask/app.py
<<<<<<< SEARCH
def factorial(n):
@ -44,7 +44,7 @@ def factorial(n):
>>>>>>> REPLACE
{fence[1]}
{fence[0]}
{fence[0]}python
mathweb/flask/app.py
<<<<<<< SEARCH
return str(factorial(n))
@ -68,7 +68,7 @@ mathweb/flask/app.py
Here are the *SEARCH/REPLACE* blocks:
{fence[0]}
{fence[0]}python
hello.py
<<<<<<< SEARCH
=======
@ -79,7 +79,7 @@ def hello():
>>>>>>> REPLACE
{fence[1]}
{fence[0]}
{fence[0]}python
main.py
<<<<<<< SEARCH
def hello():
@ -93,3 +93,51 @@ from hello import hello
""",
),
]
system_reminder = """
# *SEARCH/REPLACE block* Rules:
Every *SEARCH/REPLACE block* must use this format:
1. The opening fence and code language, eg: {fence[0]}python
2. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
3. The start of search block: <<<<<<< SEARCH
4. A contiguous chunk of lines to search for in the existing source code
5. The dividing line: =======
6. The lines to replace into the source code
7. The end of the replace block: >>>>>>> REPLACE
8. The closing fence: {fence[1]}
Use the *FULL* file path, as shown to you by the user.
{quad_backtick_reminder}
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
Including multiple unique *SEARCH/REPLACE* blocks if needed.
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
Keep *SEARCH/REPLACE* blocks concise.
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
Only create *SEARCH/REPLACE* blocks for files that the user has added to the chat!
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
Pay attention to which filenames the user wants you to edit, especially if they are asking you to create a new file.
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
- A new file path, including dir name if needed
- An empty `SEARCH` section
- The new file's contents in the `REPLACE` section
To rename files which have been added to the chat, use shell commands at the end of your response.
If the user just says something like "ok" or "go ahead" or "do that" they probably want you to make SEARCH/REPLACE blocks for the code changes you just proposed.
The user will say when they've applied your edits. If they haven't explicitly confirmed the edits have been applied, they probably want proper SEARCH/REPLACE blocks.
{lazy_prompt}
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
{shell_cmd_reminder}
"""

View file

@ -181,14 +181,17 @@ If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
- An empty `SEARCH` section
- The new file's contents in the `REPLACE` section
To rename files which have been added to the chat, use shell commands at the end of your response.
{rename_with_shell}{go_ahead_tip}{lazy_prompt}ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
{shell_cmd_reminder}
"""
If the user just says something like "ok" or "go ahead" or "do that" they probably want you to make SEARCH/REPLACE blocks for the code changes you just proposed.
rename_with_shell = """To rename files which have been added to the chat, use shell commands at the end of your response.
"""
go_ahead_tip = """If the user just says something like "ok" or "go ahead" or "do that" they probably want you to make SEARCH/REPLACE blocks for the code changes you just proposed.
The user will say when they've applied your edits. If they haven't explicitly confirmed the edits have been applied, they probably want proper SEARCH/REPLACE blocks.
{lazy_prompt}
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
{shell_cmd_reminder}
"""
shell_cmd_reminder = """
@ -200,4 +203,5 @@ Examples of when to suggest shell commands:
- Suggest OS-appropriate commands to delete or rename files/directories, or other file system operations.
- If your code changes add new dependencies, suggest the command to install them.
- Etc.
"""

View file

@ -0,0 +1,9 @@
from .editblock_fenced_coder import EditBlockFencedCoder
from .editor_diff_fenced_prompts import EditorDiffFencedPrompts
class EditorDiffFencedCoder(EditBlockFencedCoder):
"A coder that uses search/replace blocks, focused purely on editing files."
edit_format = "editor-diff-fenced"
gpt_prompts = EditorDiffFencedPrompts()

View file

@ -0,0 +1,11 @@
# flake8: noqa: E501
from .editblock_fenced_prompts import EditBlockFencedPrompts
class EditorDiffFencedPrompts(EditBlockFencedPrompts):
shell_cmd_prompt = ""
no_shell_cmd_prompt = ""
shell_cmd_reminder = ""
go_ahead_tip = ""
rename_with_shell = ""

View file

@ -14,3 +14,5 @@ ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
shell_cmd_prompt = ""
no_shell_cmd_prompt = ""
shell_cmd_reminder = ""
go_ahead_tip = ""
rename_with_shell = ""

706
aider/coders/patch_coder.py Normal file
View file

@ -0,0 +1,706 @@
import pathlib
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Tuple
from .base_coder import Coder
from .patch_prompts import PatchPrompts
# --------------------------------------------------------------------------- #
# Domain objects & Exceptions (Adapted from apply_patch.py)
# --------------------------------------------------------------------------- #
class DiffError(ValueError):
"""Any problem detected while parsing or applying a patch."""
class ActionType(str, Enum):
ADD = "Add"
DELETE = "Delete"
UPDATE = "Update"
@dataclass
class Chunk:
orig_index: int = -1 # Line number in the *original* file block where the change starts
del_lines: List[str] = field(default_factory=list)
ins_lines: List[str] = field(default_factory=list)
@dataclass
class PatchAction:
type: ActionType
path: str
# For ADD:
new_content: Optional[str] = None
# For UPDATE:
chunks: List[Chunk] = field(default_factory=list)
move_path: Optional[str] = None
# Type alias for the return type of get_edits
EditResult = Tuple[str, PatchAction]
@dataclass
class Patch:
actions: Dict[str, PatchAction] = field(default_factory=dict)
fuzz: int = 0 # Track fuzziness used during parsing
# --------------------------------------------------------------------------- #
# Helper functions (Adapted from apply_patch.py)
# --------------------------------------------------------------------------- #
def _norm(line: str) -> str:
"""Strip CR so comparisons work for both LF and CRLF input."""
return line.rstrip("\r")
def find_context_core(lines: List[str], context: List[str], start: int) -> Tuple[int, int]:
"""Finds context block, returns start index and fuzz level."""
if not context:
return start, 0
# Exact match
for i in range(start, len(lines) - len(context) + 1):
if lines[i : i + len(context)] == context:
return i, 0
# Rstrip match
norm_context = [s.rstrip() for s in context]
for i in range(start, len(lines) - len(context) + 1):
if [s.rstrip() for s in lines[i : i + len(context)]] == norm_context:
return i, 1 # Fuzz level 1
# Strip match
norm_context_strip = [s.strip() for s in context]
for i in range(start, len(lines) - len(context) + 1):
if [s.strip() for s in lines[i : i + len(context)]] == norm_context_strip:
return i, 100 # Fuzz level 100
return -1, 0
def find_context(lines: List[str], context: List[str], start: int, eof: bool) -> Tuple[int, int]:
"""Finds context, handling EOF marker."""
if eof:
# If EOF marker, first try matching at the very end
if len(lines) >= len(context):
new_index, fuzz = find_context_core(lines, context, len(lines) - len(context))
if new_index != -1:
return new_index, fuzz
# If not found at end, search from `start` as fallback
new_index, fuzz = find_context_core(lines, context, start)
return new_index, fuzz + 10_000 # Add large fuzz penalty if EOF wasn't at end
# Normal case: search from `start`
return find_context_core(lines, context, start)
def peek_next_section(lines: List[str], index: int) -> Tuple[List[str], List[Chunk], int, bool]:
"""
Parses one section (context, -, + lines) of an Update block.
Returns: (context_lines, chunks_in_section, next_index, is_eof)
"""
context_lines: List[str] = []
del_lines: List[str] = []
ins_lines: List[str] = []
chunks: List[Chunk] = []
mode = "keep" # Start by expecting context lines
start_index = index
while index < len(lines):
line = lines[index]
norm_line = _norm(line)
# Check for section terminators
if norm_line.startswith(
(
"@@",
"*** End Patch",
"*** Update File:",
"*** Delete File:",
"*** Add File:",
"*** End of File", # Special terminator
)
):
break
if norm_line == "***": # Legacy/alternative terminator? Handle just in case.
break
if norm_line.startswith("***"): # Invalid line
raise DiffError(f"Invalid patch line found in update section: {line}")
index += 1
last_mode = mode
# Determine line type and strip prefix
if line.startswith("+"):
mode = "add"
line_content = line[1:]
elif line.startswith("-"):
mode = "delete"
line_content = line[1:]
elif line.startswith(" "):
mode = "keep"
line_content = line[1:]
elif line.strip() == "": # Treat blank lines in patch as context ' '
mode = "keep"
line_content = "" # Keep it as a blank line
else:
# Assume lines without prefix are context if format is loose,
# but strict format requires ' '. Raise error for strictness.
raise DiffError(f"Invalid line prefix in update section: {line}")
# If mode changes from add/delete back to keep, finalize the previous chunk
if mode == "keep" and last_mode != "keep":
if del_lines or ins_lines:
chunks.append(
Chunk(
# orig_index is relative to the start of the *context* block found
orig_index=len(context_lines) - len(del_lines),
del_lines=del_lines,
ins_lines=ins_lines,
)
)
del_lines, ins_lines = [], []
# Collect lines based on mode
if mode == "delete":
del_lines.append(line_content)
context_lines.append(line_content) # Deleted lines are part of the original context
elif mode == "add":
ins_lines.append(line_content)
elif mode == "keep":
context_lines.append(line_content)
# Finalize any pending chunk at the end of the section
if del_lines or ins_lines:
chunks.append(
Chunk(
orig_index=len(context_lines) - len(del_lines),
del_lines=del_lines,
ins_lines=ins_lines,
)
)
# Check for EOF marker
is_eof = False
if index < len(lines) and _norm(lines[index]) == "*** End of File":
index += 1
is_eof = True
if index == start_index and not is_eof: # Should not happen if patch is well-formed
raise DiffError("Empty patch section found.")
return context_lines, chunks, index, is_eof
def identify_files_needed(text: str) -> List[str]:
"""Extracts file paths from Update and Delete actions."""
lines = text.splitlines()
paths = set()
for line in lines:
norm_line = _norm(line)
if norm_line.startswith("*** Update File: "):
paths.add(norm_line[len("*** Update File: ") :].strip())
elif norm_line.startswith("*** Delete File: "):
paths.add(norm_line[len("*** Delete File: ") :].strip())
return list(paths)
# --------------------------------------------------------------------------- #
# PatchCoder Class Implementation
# --------------------------------------------------------------------------- #
class PatchCoder(Coder):
"""
A coder that uses a custom patch format for code modifications,
inspired by the format described in tmp.gpt41edits.txt.
Applies patches using logic adapted from the reference apply_patch.py script.
"""
edit_format = "patch"
gpt_prompts = PatchPrompts()
def get_edits(self) -> List[EditResult]:
"""
Parses the LLM response content (containing the patch) into a list of
tuples, where each tuple contains the file path and the PatchAction object.
"""
content = self.partial_response_content
if not content or not content.strip():
return []
# Check for patch sentinels
lines = content.splitlines()
if (
len(lines) < 2
or not _norm(lines[0]).startswith("*** Begin Patch")
# Allow flexible end, might be EOF or just end of stream
# or _norm(lines[-1]) != "*** End Patch"
):
# Tolerate missing sentinels if content looks like a patch action
is_patch_like = any(
_norm(line).startswith(
("@@", "*** Update File:", "*** Add File:", "*** Delete File:")
)
for line in lines
)
if not is_patch_like:
# If it doesn't even look like a patch, return empty
self.io.tool_warning("Response does not appear to be in patch format.")
return []
# If it looks like a patch but lacks sentinels, try parsing anyway but warn.
self.io.tool_warning(
"Patch format warning: Missing '*** Begin Patch'/'*** End Patch' sentinels."
)
start_index = 0
else:
start_index = 1 # Skip "*** Begin Patch"
# Identify files needed for context lookups during parsing
needed_paths = identify_files_needed(content)
current_files: Dict[str, str] = {}
for rel_path in needed_paths:
abs_path = self.abs_root_path(rel_path)
try:
# Use io.read_text to handle potential errors/encodings
file_content = self.io.read_text(abs_path)
if file_content is None:
raise DiffError(
f"File referenced in patch not found or could not be read: {rel_path}"
)
current_files[rel_path] = file_content
except FileNotFoundError:
raise DiffError(f"File referenced in patch not found: {rel_path}")
except IOError as e:
raise DiffError(f"Error reading file {rel_path}: {e}")
try:
# Parse the patch text using adapted logic
patch_obj = self._parse_patch_text(lines, start_index, current_files)
# Convert Patch object actions dict to a list of tuples (path, action)
# for compatibility with the base Coder's prepare_to_edit method.
results = []
for path, action in patch_obj.actions.items():
results.append((path, action))
return results
except DiffError as e:
# Raise as ValueError for consistency with other coders' error handling
raise ValueError(f"Error parsing patch content: {e}")
except Exception as e:
# Catch unexpected errors during parsing
raise ValueError(f"Unexpected error parsing patch: {e}")
def _parse_patch_text(
self, lines: List[str], start_index: int, current_files: Dict[str, str]
) -> Patch:
"""
Parses patch content lines into a Patch object.
Adapted from the Parser class in apply_patch.py.
"""
patch = Patch()
index = start_index
fuzz_accumulator = 0
while index < len(lines):
line = lines[index]
norm_line = _norm(line)
if norm_line == "*** End Patch":
index += 1
break # Successfully reached end
# ---------- UPDATE ---------- #
if norm_line.startswith("*** Update File: "):
path = norm_line[len("*** Update File: ") :].strip()
index += 1
if not path:
raise DiffError("Update File action missing path.")
# Optional move target
move_to = None
if index < len(lines) and _norm(lines[index]).startswith("*** Move to: "):
move_to = _norm(lines[index])[len("*** Move to: ") :].strip()
index += 1
if not move_to:
raise DiffError("Move to action missing path.")
if path not in current_files:
raise DiffError(f"Update File Error - missing file content for: {path}")
file_content = current_files[path]
existing_action = patch.actions.get(path)
if existing_action is not None:
# Merge additional UPDATE block into the existing one
if existing_action.type != ActionType.UPDATE:
raise DiffError(f"Conflicting actions for file: {path}")
new_action, index, fuzz = self._parse_update_file_sections(
lines, index, file_content
)
existing_action.chunks.extend(new_action.chunks)
if move_to:
if existing_action.move_path and existing_action.move_path != move_to:
raise DiffError(f"Conflicting move targets for file: {path}")
existing_action.move_path = move_to
fuzz_accumulator += fuzz
else:
# First UPDATE block for this file
action, index, fuzz = self._parse_update_file_sections(
lines, index, file_content
)
action.path = path
action.move_path = move_to
patch.actions[path] = action
fuzz_accumulator += fuzz
continue
# ---------- DELETE ---------- #
elif norm_line.startswith("*** Delete File: "):
path = norm_line[len("*** Delete File: ") :].strip()
index += 1
if not path:
raise DiffError("Delete File action missing path.")
existing_action = patch.actions.get(path)
if existing_action:
if existing_action.type == ActionType.DELETE:
# Duplicate delete ignore the extra block
self.io.tool_warning(f"Duplicate delete action for file: {path} ignored.")
continue
else:
raise DiffError(f"Conflicting actions for file: {path}")
if path not in current_files:
raise DiffError(
f"Delete File Error - file not found: {path}"
) # Check against known files
patch.actions[path] = PatchAction(type=ActionType.DELETE, path=path)
continue
# ---------- ADD ---------- #
elif norm_line.startswith("*** Add File: "):
path = norm_line[len("*** Add File: ") :].strip()
index += 1
if not path:
raise DiffError("Add File action missing path.")
if path in patch.actions:
raise DiffError(f"Duplicate action for file: {path}")
# Check if file exists in the context provided (should not for Add).
# Note: We only have needed files, a full check requires FS access.
# if path in current_files:
# raise DiffError(f"Add File Error - file already exists: {path}")
action, index = self._parse_add_file_content(lines, index)
action.path = path # Ensure path is set
patch.actions[path] = action
continue
# If we are here, the line is unexpected
# Allow blank lines between actions
if not norm_line.strip():
index += 1
continue
raise DiffError(f"Unknown or misplaced line while parsing patch: {line}")
# Check if we consumed the whole input or stopped early
# Tolerate missing "*** End Patch" if we processed actions
# if index < len(lines) and _norm(lines[index-1]) != "*** End Patch":
# raise DiffError("Patch parsing finished unexpectedly before end of input.")
patch.fuzz = fuzz_accumulator
return patch
def _parse_update_file_sections(
self, lines: List[str], index: int, file_content: str
) -> Tuple[PatchAction, int, int]:
"""Parses all sections (@@, context, -, +) for a single Update File action."""
action = PatchAction(type=ActionType.UPDATE, path="") # Path set by caller
orig_lines = file_content.splitlines() # Use splitlines for consistency
current_file_index = 0 # Track position in original file content
total_fuzz = 0
while index < len(lines):
norm_line = _norm(lines[index])
# Check for terminators for *this* file update
if norm_line.startswith(
(
"*** End Patch",
"*** Update File:",
"*** Delete File:",
"*** Add File:",
)
):
break # End of this file's update section
# Handle @@ scope lines (optional)
scope_lines = []
while index < len(lines) and _norm(lines[index]).startswith("@@"):
scope_line_content = lines[index][len("@@") :].strip()
if scope_line_content: # Ignore empty @@ lines?
scope_lines.append(scope_line_content)
index += 1
# Find the scope in the original file if specified
if scope_lines:
# Simple scope finding: search from current position
# A more robust finder could handle nested scopes like the reference @@ @@
found_scope = False
temp_index = current_file_index
while temp_index < len(orig_lines):
# Check if all scope lines match sequentially from temp_index
match = True
for i, scope in enumerate(scope_lines):
if (
temp_index + i >= len(orig_lines)
or _norm(orig_lines[temp_index + i]).strip() != scope
):
match = False
break
if match:
current_file_index = temp_index + len(scope_lines)
found_scope = True
break
temp_index += 1
if not found_scope:
# Try fuzzy scope matching (strip whitespace)
temp_index = current_file_index
while temp_index < len(orig_lines):
match = True
for i, scope in enumerate(scope_lines):
if (
temp_index + i >= len(orig_lines)
or _norm(orig_lines[temp_index + i]).strip() != scope.strip()
):
match = False
break
if match:
current_file_index = temp_index + len(scope_lines)
found_scope = True
total_fuzz += 1 # Add fuzz for scope match difference
break
temp_index += 1
if not found_scope:
scope_txt = "\n".join(scope_lines)
raise DiffError(f"Could not find scope context:\n{scope_txt}")
# Peek and parse the next context/change section
context_block, chunks_in_section, next_index, is_eof = peek_next_section(lines, index)
# Find where this context block appears in the original file
found_index, fuzz = find_context(orig_lines, context_block, current_file_index, is_eof)
total_fuzz += fuzz
if found_index == -1:
ctx_txt = "\n".join(context_block)
marker = "*** End of File" if is_eof else ""
raise DiffError(
f"Could not find patch context {marker} starting near line"
f" {current_file_index}:\n{ctx_txt}"
)
# Adjust chunk original indices to be absolute within the file
for chunk in chunks_in_section:
# chunk.orig_index from peek is relative to context_block start
# We need it relative to the file start
chunk.orig_index += found_index
action.chunks.append(chunk)
# Advance file index past the matched context block
current_file_index = found_index + len(context_block)
# Advance line index past the processed section in the patch
index = next_index
return action, index, total_fuzz
def _parse_add_file_content(self, lines: List[str], index: int) -> Tuple[PatchAction, int]:
"""Parses the content (+) lines for an Add File action."""
added_lines: List[str] = []
while index < len(lines):
line = lines[index]
norm_line = _norm(line)
# Stop if we hit another action or end marker
if norm_line.startswith(
(
"*** End Patch",
"*** Update File:",
"*** Delete File:",
"*** Add File:",
)
):
break
# Expect lines to start with '+'
if not line.startswith("+"):
# Tolerate blank lines? Or require '+'? Reference implies '+' required.
if norm_line.strip() == "":
# Treat blank line as adding a blank line
added_lines.append("")
else:
raise DiffError(f"Invalid Add File line (missing '+'): {line}")
else:
added_lines.append(line[1:]) # Strip leading '+'
index += 1
action = PatchAction(type=ActionType.ADD, path="", new_content="\n".join(added_lines))
return action, index
def apply_edits(self, edits: List[PatchAction]):
"""
Applies the parsed PatchActions to the corresponding files.
"""
if not edits:
return
# Group edits by original path? Not strictly needed if processed sequentially.
# Edits are now List[Tuple[str, PatchAction]]
for _path_tuple_element, action in edits:
# action is the PatchAction object
# action.path is the canonical path within the action logic
full_path = self.abs_root_path(action.path)
path_obj = pathlib.Path(full_path)
try:
if action.type == ActionType.ADD:
# Check existence *before* writing
if path_obj.exists():
raise DiffError(f"ADD Error: File already exists: {action.path}")
if action.new_content is None:
# Parser should ensure this doesn't happen
raise DiffError(f"ADD change for {action.path} has no content")
self.io.tool_output(f"Adding {action.path}")
path_obj.parent.mkdir(parents=True, exist_ok=True)
# Ensure single trailing newline, matching reference behavior
content_to_write = action.new_content
if not content_to_write.endswith("\n"):
content_to_write += "\n"
self.io.write_text(full_path, content_to_write)
elif action.type == ActionType.DELETE:
self.io.tool_output(f"Deleting {action.path}")
if not path_obj.exists():
self.io.tool_warning(
f"DELETE Warning: File not found, skipping: {action.path}"
)
else:
path_obj.unlink()
elif action.type == ActionType.UPDATE:
if not path_obj.exists():
raise DiffError(f"UPDATE Error: File does not exist: {action.path}")
current_content = self.io.read_text(full_path)
if current_content is None:
# Should have been caught during parsing if file was needed
raise DiffError(f"Could not read file for UPDATE: {action.path}")
# Apply the update logic using the parsed chunks
new_content = self._apply_update(current_content, action, action.path)
target_full_path = (
self.abs_root_path(action.move_path) if action.move_path else full_path
)
target_path_obj = pathlib.Path(target_full_path)
if action.move_path:
self.io.tool_output(
f"Updating and moving {action.path} to {action.move_path}"
)
# Check if target exists before overwriting/moving
if target_path_obj.exists() and full_path != target_full_path:
self.io.tool_warning(
"UPDATE Warning: Target file for move already exists, overwriting:"
f" {action.move_path}"
)
else:
self.io.tool_output(f"Updating {action.path}")
# Ensure parent directory exists for target
target_path_obj.parent.mkdir(parents=True, exist_ok=True)
self.io.write_text(target_full_path, new_content)
# Remove original file *after* successful write to new location if moved
if action.move_path and full_path != target_full_path:
path_obj.unlink()
else:
# Should not happen
raise DiffError(f"Unknown action type encountered: {action.type}")
except (DiffError, FileNotFoundError, IOError, OSError) as e:
# Raise a ValueError to signal failure, consistent with other coders.
raise ValueError(f"Error applying action '{action.type}' to {action.path}: {e}")
except Exception as e:
# Catch unexpected errors during application
raise ValueError(
f"Unexpected error applying action '{action.type}' to {action.path}: {e}"
)
def _apply_update(self, text: str, action: PatchAction, path: str) -> str:
"""
Applies UPDATE chunks to the given text content.
Adapted from _get_updated_file in apply_patch.py.
"""
if action.type is not ActionType.UPDATE:
# Should not be called otherwise, but check for safety
raise DiffError("_apply_update called with non-update action")
orig_lines = text.splitlines() # Use splitlines to handle endings consistently
dest_lines: List[str] = []
current_orig_line_idx = 0 # Tracks index in orig_lines processed so far
# Sort chunks by their original index to apply them sequentially
sorted_chunks = sorted(action.chunks, key=lambda c: c.orig_index)
for chunk in sorted_chunks:
# chunk.orig_index is the absolute line number where the change starts
# (where the first deleted line was, or where inserted lines go if no deletes)
chunk_start_index = chunk.orig_index
if chunk_start_index < current_orig_line_idx:
# This indicates overlapping chunks or incorrect indices from parsing
raise DiffError(
f"{path}: Overlapping or out-of-order chunk detected."
f" Current index {current_orig_line_idx}, chunk starts at {chunk_start_index}."
)
# Add lines from original file between the last chunk and this one
dest_lines.extend(orig_lines[current_orig_line_idx:chunk_start_index])
# Verify that the lines to be deleted actually match the original file content
# (The parser should have used find_context, but double-check here)
num_del = len(chunk.del_lines)
actual_deleted_lines = orig_lines[chunk_start_index : chunk_start_index + num_del]
# Use the same normalization as find_context_core for comparison robustness
norm_chunk_del = [_norm(s).strip() for s in chunk.del_lines]
norm_actual_del = [_norm(s).strip() for s in actual_deleted_lines]
if norm_chunk_del != norm_actual_del:
# This indicates the context matching failed or the file changed since parsing
# Provide detailed error message
expected_str = "\n".join(f"- {s}" for s in chunk.del_lines)
actual_str = "\n".join(f" {s}" for s in actual_deleted_lines)
raise DiffError(
f"{path}: Mismatch applying patch near line {chunk_start_index + 1}.\n"
f"Expected lines to remove:\n{expected_str}\n"
f"Found lines in file:\n{actual_str}"
)
# Add the inserted lines from the chunk
dest_lines.extend(chunk.ins_lines)
# Advance the original line index past the lines processed (deleted lines)
current_orig_line_idx = chunk_start_index + num_del
# Add any remaining lines from the original file after the last chunk
dest_lines.extend(orig_lines[current_orig_line_idx:])
# Join lines and ensure a single trailing newline
result = "\n".join(dest_lines)
if result or orig_lines: # Add newline unless result is empty and original was empty
result += "\n"
return result

View file

@ -0,0 +1,161 @@
# flake8: noqa: E501
from .base_prompts import CoderPrompts
from .editblock_prompts import EditBlockPrompts
class PatchPrompts(EditBlockPrompts):
# --------------------------------------------------------------------- #
# SYSTEM PROMPT
# --------------------------------------------------------------------- #
main_system = """Act as an expert software developer.
Always use best practices when coding.
Respect and use existing conventions, libraries, etc that are already present in the code base.
{lazy_prompt}
Take requests for changes to the supplied code.
If the request is ambiguous, ask questions.
Always reply to the user in {language}.
Once you understand the request you MUST:
1. Decide if you need to propose edits to any files that haven't been added to the chat. You can create new files without asking!
If you need to propose edits to existing files not already added to the chat, you *MUST* tell the user their full path names and ask them to *add the files to the chat*.
End your reply and wait for their approval.
You can keep asking if you then decide you need to edit more files.
2. Think stepbystep and explain the needed changes in a few short sentences.
3. Describe the changes using the V4A diff format, enclosed within `*** Begin Patch` and `*** End Patch` markers.
IMPORTANT: Each file MUST appear only once in the patch.
Consolidate **all** edits for a given file into a single `*** [ACTION] File:` block.
{shell_cmd_prompt}
"""
# --------------------------------------------------------------------- #
# EXAMPLE MESSAGES
# --------------------------------------------------------------------- #
example_messages = [
# --- Example 1 ---------------------------------------------------- #
dict(
role="user",
content="Change get_factorial() to use math.factorial",
),
dict(
role="assistant",
content="""To make this change we need to modify `mathweb/flask/app.py`:
1. Import the `math` module.
2. Replace the custom recursive `factorial()` helper with `math.factorial`.
3. Update `get_factorial()` to call `math.factorial`.
Here is the patch:
*** Begin Patch
*** Update File: mathweb/flask/app.py
@@
-from flask import Flask
-
-from flask import Flask
+from flask import Flask
+import math
@@
-def factorial(n):
- "compute factorial"
-
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
+def factorial(n):
+ "compute factorial"
+
+ return math.factorial(n)
@@ def get_factorial(self, n):
- return str(factorial(n))
+ return str(math.factorial(n))
*** End Patch
""",
),
# --- Example 2 ---------------------------------------------------- #
dict(
role="user",
content="Refactor hello() into its own file.",
),
dict(
role="assistant",
content="""To make this change we need to modify `main.py` and create a new file `hello.py`:
1. Create `hello.py` with the `hello()` function.
2. Remove `hello()` from `main.py` and import it instead.
Here is the patch:
*** Begin Patch
*** Add File: hello.py
+# A simple function
+def hello():
+ "print a greeting"
+
+ print("hello")
*** Update File: main.py
@@
-def hello():
- "print a greeting"
-
- print("hello")
+from hello import hello
*** End Patch
""",
),
]
# --------------------------------------------------------------------- #
# SYSTEM REMINDER
# --------------------------------------------------------------------- #
system_reminder = """# V4A Diff Format Rules:
Your entire response containing the patch MUST start with `*** Begin Patch` on a line by itself.
Your entire response containing the patch MUST end with `*** End Patch` on a line by itself.
Use the *FULL* file path, as shown to you by the user.
{quad_backtick_reminder}
For each file you need to modify, start with a marker line:
*** [ACTION] File: [path/to/file]
Where `[ACTION]` is one of `Add`, `Update`, or `Delete`.
**Each file MUST appear only once in the patch.**
Consolidate all changes for that file into the same block.
If you are moving code within a file, include both the deletions and the
insertions as separate hunks inside this single `*** Update File:` block
(do *not* open a second block for the same file).
For `Update` actions, describe each snippet of code that needs to be changed using the following format:
1. Context lines: Include 3 lines of context *before* the change. These lines MUST start with a single space ` `.
2. Lines to remove: Precede each line to be removed with a minus sign `-`.
3. Lines to add: Precede each line to be added with a plus sign `+`.
4. Context lines: Include 3 lines of context *after* the change. These lines MUST start with a single space ` `.
Context lines MUST exactly match the existing file content, character for character, including indentation.
If a change is near the beginning or end of the file, include fewer than 3 context lines as appropriate.
If 3 lines of context is insufficient to uniquely identify the snippet, use `@@ [CLASS_OR_FUNCTION_NAME]` markers on their own lines *before* the context lines to specify the scope. You can use multiple `@@` markers if needed.
Do not include line numbers.
Only create patches for files that the user has added to the chat!
When moving code *within* a single file, keep everything inside one
`*** Update File:` block. Provide one hunk that deletes the code from its
original location and another hunk that inserts it at the new location.
For `Add` actions, use the `*** Add File: [path/to/new/file]` marker, followed by the lines of the new file, each preceded by a plus sign `+`.
For `Delete` actions, use the `*** Delete File: [path/to/file]` marker. No other lines are needed for the deletion.
{rename_with_shell}{go_ahead_tip}{lazy_prompt}ONLY EVER RETURN CODE IN THE SPECIFIED V4A DIFF FORMAT!
{shell_cmd_reminder}
"""

View file

@ -235,20 +235,6 @@ Left
Left
"""
"""
ri = RelativeIndenter([example])
dump(example)
rel_example = ri.make_relative(example)
dump(repr(rel_example))
abs_example = ri.make_absolute(rel_example)
dump(abs_example)
sys.exit()
"""
def relative_indent(texts):
ri = RelativeIndenter(texts)
@ -349,7 +335,7 @@ def lines_to_chars(lines, mapping):
return new_text
def dmp_lines_apply(texts, remap=True):
def dmp_lines_apply(texts):
debug = False
# debug = True
@ -655,8 +641,6 @@ def proc(dname):
(dmp_lines_apply, all_preprocs),
]
_strategies = editblock_strategies # noqa: F841
short_names = dict(
search_and_replace="sr",
git_cherry_pick_osr_onto_o="cp_o",

View file

@ -45,6 +45,7 @@ other_hunks_applied = (
class UnifiedDiffCoder(Coder):
"""A coder that uses unified diff format for code modifications."""
edit_format = "udiff"
gpt_prompts = UnifiedDiffPrompts()
@ -344,7 +345,16 @@ def process_fenced_block(lines, start_line_num):
if block[0].startswith("--- ") and block[1].startswith("+++ "):
# Extract the file path, considering that it might contain spaces
fname = block[1][4:].strip()
a_fname = block[0][4:].strip()
b_fname = block[1][4:].strip()
# Check if standard git diff prefixes are present (or /dev/null) and strip them
if (a_fname.startswith("a/") or a_fname == "/dev/null") and b_fname.startswith("b/"):
fname = b_fname[2:]
else:
# Otherwise, assume the path is as intended
fname = b_fname
block = block[2:]
else:
fname = None

View file

@ -0,0 +1,14 @@
from .udiff_coder import UnifiedDiffCoder
from .udiff_simple_prompts import UnifiedDiffSimplePrompts
class UnifiedDiffSimpleCoder(UnifiedDiffCoder):
"""
A coder that uses unified diff format for code modifications.
This variant uses a simpler prompt that doesn't mention specific
diff rules like using `@@ ... @@` lines or avoiding line numbers.
"""
edit_format = "udiff-simple"
gpt_prompts = UnifiedDiffSimplePrompts()

View file

@ -0,0 +1,25 @@
from .udiff_prompts import UnifiedDiffPrompts
class UnifiedDiffSimplePrompts(UnifiedDiffPrompts):
"""
Prompts for the UnifiedDiffSimpleCoder.
Inherits from UnifiedDiffPrompts and can override specific prompts
if a simpler wording is desired for this edit format.
"""
example_messages = []
system_reminder = """# File editing rules:
Return edits similar to unified diffs that `diff -U0` would produce.
The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file!
Think carefully and make sure you include and mark all lines that need to be removed or changed as `-` lines.
Make sure you mark all new or modified lines with `+`.
Don't leave out any lines or the diff patch won't apply correctly.
To make a new file, show a diff from `--- /dev/null` to `+++ path/to/new/file.ext`.
{lazy_prompt}
"""

View file

@ -17,6 +17,7 @@ from aider import models, prompts, voice
from aider.editor import pipe_editor
from aider.format_settings import format_settings
from aider.help import Help, install_help_extra
from aider.io import CommandCompletionException
from aider.llm import litellm
from aider.repo import ANY_GIT_ERROR
from aider.run_cmd import run_cmd
@ -27,8 +28,9 @@ from .dump import dump # noqa: F401
class SwitchCoder(Exception):
def __init__(self, **kwargs):
def __init__(self, placeholder=None, **kwargs):
self.kwargs = kwargs
self.placeholder = placeholder
class Commands:
@ -59,6 +61,7 @@ class Commands:
parser=None,
verbose=False,
editor=None,
original_read_only_fnames=None,
):
self.io = io
self.coder = coder
@ -77,11 +80,52 @@ class Commands:
self.help = None
self.editor = editor
# Store the original read-only filenames provided via args.read
self.original_read_only_fnames = set(original_read_only_fnames or [])
def cmd_model(self, args):
"Switch to a new LLM"
"Switch the Main Model to a new LLM"
model_name = args.strip()
model = models.Model(model_name, weak_model=self.coder.main_model.weak_model.name)
model = models.Model(
model_name,
editor_model=self.coder.main_model.editor_model.name,
weak_model=self.coder.main_model.weak_model.name,
)
models.sanity_check_models(self.io, model)
# Check if the current edit format is the default for the old model
old_model_edit_format = self.coder.main_model.edit_format
current_edit_format = self.coder.edit_format
new_edit_format = current_edit_format
if current_edit_format == old_model_edit_format:
# If the user was using the old model's default, switch to the new model's default
new_edit_format = model.edit_format
raise SwitchCoder(main_model=model, edit_format=new_edit_format)
def cmd_editor_model(self, args):
"Switch the Editor Model to a new LLM"
model_name = args.strip()
model = models.Model(
self.coder.main_model.name,
editor_model=model_name,
weak_model=self.coder.main_model.weak_model.name,
)
models.sanity_check_models(self.io, model)
raise SwitchCoder(main_model=model)
def cmd_weak_model(self, args):
"Switch the Weak Model to a new LLM"
model_name = args.strip()
model = models.Model(
self.coder.main_model.name,
editor_model=self.coder.main_model.editor_model.name,
weak_model=model_name,
)
models.sanity_check_models(self.io, model)
raise SwitchCoder(main_model=model)
@ -114,6 +158,10 @@ class Commands:
" them."
),
),
(
"context",
"Automatically identify which files will need to be edited.",
),
]
)
@ -355,7 +403,21 @@ class Commands:
def _drop_all_files(self):
self.coder.abs_fnames = set()
self.coder.abs_read_only_fnames = set()
# When dropping all files, keep those that were originally provided via args.read
if self.original_read_only_fnames:
# Keep only the original read-only files
to_keep = set()
for abs_fname in self.coder.abs_read_only_fnames:
rel_fname = self.coder.get_rel_fname(abs_fname)
if (
abs_fname in self.original_read_only_fnames
or rel_fname in self.original_read_only_fnames
):
to_keep.add(abs_fname)
self.coder.abs_read_only_fnames = to_keep
else:
self.coder.abs_read_only_fnames = set()
def _clear_chat_history(self):
self.coder.done_messages = []
@ -822,7 +884,12 @@ class Commands:
"Remove files from the chat session to free up context space"
if not args.strip():
self.io.tool_output("Dropping all files from the chat session.")
if self.original_read_only_fnames:
self.io.tool_output(
"Dropping all files from the chat session except originally read-only files."
)
else:
self.io.tool_output("Dropping all files from the chat session.")
self._drop_all_files()
return
@ -947,9 +1014,15 @@ class Commands:
dict(role="assistant", content="Ok."),
]
if add and exit_status != 0:
if add_on_nonzero_exit and exit_status != 0:
# Return the formatted output message for test failures
return msg
elif add and exit_status != 0:
self.io.placeholder = "What's wrong? Fix"
# Return None if output wasn't added or command succeeded
return None
def cmd_exit(self, args):
"Exit the application"
self.coder.event("exit", reason="/exit")
@ -1067,6 +1140,18 @@ class Commands:
def cmd_batch(self, args):
"""Iteratively perform the change on files in batches that fit to context and output limits"""
return self._generic_chat_command(args, "batch")
def completions_ask(self):
raise CommandCompletionException()
def completions_code(self):
raise CommandCompletionException()
def completions_architect(self):
raise CommandCompletionException()
def completions_context(self):
raise CommandCompletionException()
def cmd_ask(self, args):
"""Ask questions about the code base without editing any files. If no prompt provided, switches to ask mode.""" # noqa
return self._generic_chat_command(args, "ask")
@ -1079,7 +1164,11 @@ class Commands:
"""Enter architect/editor mode using 2 different models. If no prompt provided, switches to architect/editor mode.""" # noqa
return self._generic_chat_command(args, "architect")
def _generic_chat_command(self, args, edit_format):
def cmd_context(self, args):
"""Enter context mode to see surrounding code context. If no prompt provided, switches to context mode.""" # noqa
return self._generic_chat_command(args, "context", placeholder=args.strip() or None)
def _generic_chat_command(self, args, edit_format, placeholder=None):
if not args.strip():
# Switch to the corresponding chat mode if no args provided
return self.cmd_chat_mode(edit_format)
@ -1096,11 +1185,13 @@ class Commands:
user_msg = args
coder.run(user_msg)
# Use the provided placeholder if any
raise SwitchCoder(
edit_format=self.coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
show_announcements=False,
placeholder=placeholder,
)
def get_help_md(self):
@ -1413,6 +1504,62 @@ class Commands:
if user_input.strip():
self.io.set_placeholder(user_input.rstrip())
def cmd_edit(self, args=""):
"Alias for /editor: Open an editor to write a prompt"
return self.cmd_editor(args)
def cmd_think_tokens(self, args):
"Set the thinking token budget (supports formats like 8096, 8k, 10.5k, 0.5M)"
model = self.coder.main_model
if not args.strip():
# Display current value if no args are provided
formatted_budget = model.get_thinking_tokens()
if formatted_budget is None:
self.io.tool_output("Thinking tokens are not currently set.")
else:
budget = model.get_raw_thinking_tokens()
self.io.tool_output(
f"Current thinking token budget: {budget:,} tokens ({formatted_budget})."
)
return
value = args.strip()
model.set_thinking_tokens(value)
formatted_budget = model.get_thinking_tokens()
budget = model.get_raw_thinking_tokens()
self.io.tool_output(f"Set thinking token budget to {budget:,} tokens ({formatted_budget}).")
self.io.tool_output()
# Output announcements
announcements = "\n".join(self.coder.get_announcements())
self.io.tool_output(announcements)
def cmd_reasoning_effort(self, args):
"Set the reasoning effort level (values: number or low/medium/high depending on model)"
model = self.coder.main_model
if not args.strip():
# Display current value if no args are provided
reasoning_value = model.get_reasoning_effort()
if reasoning_value is None:
self.io.tool_output("Reasoning effort is not currently set.")
else:
self.io.tool_output(f"Current reasoning effort: {reasoning_value}")
return
value = args.strip()
model.set_reasoning_effort(value)
reasoning_value = model.get_reasoning_effort()
self.io.tool_output(f"Set reasoning effort to {reasoning_value}")
self.io.tool_output()
# Output announcements
announcements = "\n".join(self.coder.get_announcements())
self.io.tool_output(announcements)
def cmd_copy_context(self, args=None):
"""Copy the current chat context as markdown, suitable to paste into a web UI"""

126
aider/deprecated.py Normal file
View file

@ -0,0 +1,126 @@
def add_deprecated_model_args(parser, group):
"""Add deprecated model shortcut arguments to the argparse parser."""
opus_model = "claude-3-opus-20240229"
group.add_argument(
"--opus",
action="store_true",
help=f"Use {opus_model} model for the main chat (deprecated, use --model)",
default=False,
)
sonnet_model = "anthropic/claude-3-7-sonnet-20250219"
group.add_argument(
"--sonnet",
action="store_true",
help=f"Use {sonnet_model} model for the main chat (deprecated, use --model)",
default=False,
)
haiku_model = "claude-3-5-haiku-20241022"
group.add_argument(
"--haiku",
action="store_true",
help=f"Use {haiku_model} model for the main chat (deprecated, use --model)",
default=False,
)
gpt_4_model = "gpt-4-0613"
group.add_argument(
"--4",
"-4",
action="store_true",
help=f"Use {gpt_4_model} model for the main chat (deprecated, use --model)",
default=False,
)
gpt_4o_model = "gpt-4o"
group.add_argument(
"--4o",
action="store_true",
help=f"Use {gpt_4o_model} model for the main chat (deprecated, use --model)",
default=False,
)
gpt_4o_mini_model = "gpt-4o-mini"
group.add_argument(
"--mini",
action="store_true",
help=f"Use {gpt_4o_mini_model} model for the main chat (deprecated, use --model)",
default=False,
)
gpt_4_turbo_model = "gpt-4-1106-preview"
group.add_argument(
"--4-turbo",
action="store_true",
help=f"Use {gpt_4_turbo_model} model for the main chat (deprecated, use --model)",
default=False,
)
gpt_3_model_name = "gpt-3.5-turbo"
group.add_argument(
"--35turbo",
"--35-turbo",
"--3",
"-3",
action="store_true",
help=f"Use {gpt_3_model_name} model for the main chat (deprecated, use --model)",
default=False,
)
deepseek_model = "deepseek/deepseek-chat"
group.add_argument(
"--deepseek",
action="store_true",
help=f"Use {deepseek_model} model for the main chat (deprecated, use --model)",
default=False,
)
o1_mini_model = "o1-mini"
group.add_argument(
"--o1-mini",
action="store_true",
help=f"Use {o1_mini_model} model for the main chat (deprecated, use --model)",
default=False,
)
o1_preview_model = "o1-preview"
group.add_argument(
"--o1-preview",
action="store_true",
help=f"Use {o1_preview_model} model for the main chat (deprecated, use --model)",
default=False,
)
def handle_deprecated_model_args(args, io):
"""Handle deprecated model shortcut arguments and provide appropriate warnings."""
# Define model mapping
model_map = {
"opus": "claude-3-opus-20240229",
"sonnet": "anthropic/claude-3-7-sonnet-20250219",
"haiku": "claude-3-5-haiku-20241022",
"4": "gpt-4-0613",
"4o": "gpt-4o",
"mini": "gpt-4o-mini",
"4_turbo": "gpt-4-1106-preview",
"35turbo": "gpt-3.5-turbo",
"deepseek": "deepseek/deepseek-chat",
"o1_mini": "o1-mini",
"o1_preview": "o1-preview",
}
# Check if any deprecated args are used
for arg_name, model_name in model_map.items():
arg_name_clean = arg_name.replace("-", "_")
if hasattr(args, arg_name_clean) and getattr(args, arg_name_clean):
# Find preferred name to display in warning
from aider.models import MODEL_ALIASES
display_name = model_name
# Check if there's a shorter alias for this model
for alias, full_name in MODEL_ALIASES.items():
if full_name == model_name:
display_name = alias
break
# Show the warning
io.tool_warning(
f"The --{arg_name.replace('_', '-')} flag is deprecated and will be removed in a"
f" future version. Please use --model {display_name} instead."
)
# Set the model
if not args.model:
args.model = model_name
break

View file

@ -83,4 +83,25 @@ class LiteLLMExceptions:
)
if "boto3" in str(ex):
return ExInfo("APIConnectionError", False, "You need to: pip install boto3")
if "OpenrouterException" in str(ex) and "'choices'" in str(ex):
return ExInfo(
"APIConnectionError",
True,
(
"OpenRouter or the upstream API provider is down, overloaded or rate"
" limiting your requests."
),
)
# Check for specific non-retryable APIError cases like insufficient credits
if ex.__class__ is litellm.APIError:
err_str = str(ex).lower()
if "insufficient credits" in err_str and '"code":402' in err_str:
return ExInfo(
"APIError",
False,
"Insufficient credits with the API provider. Please add credits.",
)
# Fall through to default APIError handling if not the specific credits error
return self.exceptions.get(ex.__class__, ExInfo(None, None, None))

View file

@ -10,4 +10,10 @@ exclude_website_pats = [
"docs/unified-diffs.md",
"docs/leaderboards/index.md",
"assets/**",
".jekyll-metadata",
"Gemfile.lock",
"Gemfile",
"_config.yml",
"**/OLD/**",
"OLD/**",
]

View file

@ -1,7 +1,9 @@
import base64
import functools
import os
import shutil
import signal
import subprocess
import time
import webbrowser
from collections import defaultdict
@ -16,6 +18,7 @@ from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters import Condition, is_searching
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.vi_state import InputMode
from prompt_toolkit.keys import Keys
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.output.vt100 import is_dumb_terminal
@ -23,6 +26,7 @@ from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
from prompt_toolkit.styles import Style
from pygments.lexers import MarkdownLexer, guess_lexer_for_filename
from pygments.token import Token
from rich.color import ColorParseError
from rich.columns import Columns
from rich.console import Console
from rich.markdown import Markdown
@ -32,8 +36,23 @@ from rich.text import Text
from aider.mdstream import MarkdownStream
from .dump import dump # noqa: F401
from .editor import pipe_editor
from .utils import is_image_file
# Constants
NOTIFICATION_MESSAGE = "Aider is waiting for your input"
def ensure_hash_prefix(color):
"""Ensure hex color values have a # prefix."""
if not color:
return color
if isinstance(color, str) and color.strip() and not color.startswith("#"):
# Check if it's a valid hex color (3 or 6 hex digits)
if all(c in "0123456789ABCDEFabcdef" for c in color) and len(color) in (3, 6):
return f"#{color}"
return color
def restore_multiline(func):
"""Decorator to restore multiline mode after function execution"""
@ -52,6 +71,13 @@ def restore_multiline(func):
return wrapper
class CommandCompletionException(Exception):
"""Raised when a command should use the normal autocompleter instead of
command-specific completion."""
pass
@dataclass
class ConfirmGroup:
preference: str = None
@ -170,14 +196,23 @@ class AutoCompleter(Completer):
return
if text[0] == "/":
yield from self.get_command_completions(document, complete_event, text, words)
return
try:
yield from self.get_command_completions(document, complete_event, text, words)
return
except CommandCompletionException:
# Fall through to normal completion
pass
candidates = self.words
candidates.update(set(self.fname_to_rel_fnames))
candidates = [word if type(word) is tuple else (word, word) for word in candidates]
last_word = words[-1]
# Only provide completions if the user has typed at least 3 characters
if len(last_word) < 3:
return
completions = []
for word_match, word_insert in candidates:
if word_match.lower().startswith(last_word.lower()):
@ -196,6 +231,8 @@ class InputOutput:
num_error_outputs = 0
num_user_asks = 0
clipboard_watcher = None
bell_on_next_input = False
notifications_command = None
def __init__(
self,
@ -224,25 +261,40 @@ class InputOutput:
file_watcher=None,
multiline_mode=False,
root=".",
notifications=False,
notifications_command=None,
):
self.placeholder = None
self.interrupted = False
self.never_prompts = set()
self.editingmode = editingmode
self.multiline_mode = multiline_mode
self.bell_on_next_input = False
self.notifications = notifications
if notifications and notifications_command is None:
self.notifications_command = self.get_default_notification_command()
else:
self.notifications_command = notifications_command
no_color = os.environ.get("NO_COLOR")
if no_color is not None and no_color != "":
pretty = False
self.user_input_color = user_input_color if pretty else None
self.tool_output_color = tool_output_color if pretty else None
self.tool_error_color = tool_error_color if pretty else None
self.tool_warning_color = tool_warning_color if pretty else None
self.assistant_output_color = assistant_output_color
self.completion_menu_color = completion_menu_color if pretty else None
self.completion_menu_bg_color = completion_menu_bg_color if pretty else None
self.completion_menu_current_color = completion_menu_current_color if pretty else None
self.completion_menu_current_bg_color = completion_menu_current_bg_color if pretty else None
self.user_input_color = ensure_hash_prefix(user_input_color) if pretty else None
self.tool_output_color = ensure_hash_prefix(tool_output_color) if pretty else None
self.tool_error_color = ensure_hash_prefix(tool_error_color) if pretty else None
self.tool_warning_color = ensure_hash_prefix(tool_warning_color) if pretty else None
self.assistant_output_color = ensure_hash_prefix(assistant_output_color)
self.completion_menu_color = ensure_hash_prefix(completion_menu_color) if pretty else None
self.completion_menu_bg_color = (
ensure_hash_prefix(completion_menu_bg_color) if pretty else None
)
self.completion_menu_current_color = (
ensure_hash_prefix(completion_menu_current_color) if pretty else None
)
self.completion_menu_current_bg_color = (
ensure_hash_prefix(completion_menu_current_bg_color) if pretty else None
)
self.code_theme = code_theme
@ -310,6 +362,35 @@ class InputOutput:
self.file_watcher = file_watcher
self.root = root
# Validate color settings after console is initialized
self._validate_color_settings()
def _validate_color_settings(self):
"""Validate configured color strings and reset invalid ones."""
color_attributes = [
"user_input_color",
"tool_output_color",
"tool_error_color",
"tool_warning_color",
"assistant_output_color",
"completion_menu_color",
"completion_menu_bg_color",
"completion_menu_current_color",
"completion_menu_current_bg_color",
]
for attr_name in color_attributes:
color_value = getattr(self, attr_name, None)
if color_value:
try:
# Try creating a style to validate the color
RichStyle(color=color_value)
except ColorParseError as e:
self.console.print(
"[bold red]Warning:[/bold red] Invalid configuration for"
f" {attr_name}: '{color_value}'. {e}. Disabling this color."
)
setattr(self, attr_name, None) # Reset invalid color to None
def _get_style(self):
style_dict = {}
if not self.pretty:
@ -335,9 +416,9 @@ class InputOutput:
# Conditionally add 'completion-menu.completion.current' style
completion_menu_current_style = []
if self.completion_menu_current_bg_color:
completion_menu_current_style.append(f"bg:{self.completion_menu_current_bg_color}")
completion_menu_current_style.append(self.completion_menu_current_bg_color)
if self.completion_menu_current_color:
completion_menu_current_style.append(self.completion_menu_current_color)
completion_menu_current_style.append(f"bg:{self.completion_menu_current_color}")
if completion_menu_current_style:
style_dict["completion-menu.completion.current"] = " ".join(
completion_menu_current_style
@ -444,6 +525,9 @@ class InputOutput:
):
self.rule()
# Ring the bell if needed
self.ring_bell()
rel_fnames = list(rel_fnames)
show = ""
if rel_fnames:
@ -451,11 +535,16 @@ class InputOutput:
get_rel_fname(fname, root) for fname in (abs_read_only_fnames or [])
]
show = self.format_files_for_input(rel_fnames, rel_read_only_fnames)
prompt_prefix = ""
if edit_format:
show += edit_format
prompt_prefix += edit_format
if self.multiline_mode:
show += (" " if edit_format else "") + "multi"
show += "> "
prompt_prefix += (" " if edit_format else "") + "multi"
prompt_prefix += "> "
show += prompt_prefix
self.prompt_prefix = prompt_prefix
inp = ""
multiline_input = False
@ -499,11 +588,30 @@ class InputOutput:
"Navigate forward through history"
event.current_buffer.history_forward()
@kb.add("c-x", "c-e")
def _(event):
"Edit current input in external editor (like Bash)"
buffer = event.current_buffer
current_text = buffer.text
# Open the editor with the current text
edited_text = pipe_editor(input_data=current_text)
# Replace the buffer with the edited text, strip any trailing newlines
buffer.text = edited_text.rstrip("\n")
# Move cursor to the end of the text
buffer.cursor_position = len(buffer.text)
@kb.add("enter", eager=True, filter=~is_searching)
def _(event):
"Handle Enter key press"
if self.multiline_mode:
# In multiline mode, Enter adds a newline
if self.multiline_mode and not (
self.editingmode == EditingMode.VI
and event.app.vi_state.input_mode == InputMode.NAVIGATION
):
# In multiline mode and if not in vi-mode or vi navigation/normal mode,
# Enter adds a newline
event.current_buffer.insert_text("\n")
else:
# In normal mode, Enter submits
@ -521,7 +629,7 @@ class InputOutput:
while True:
if multiline_input:
show = ". "
show = self.prompt_prefix
try:
if self.prompt_session:
@ -537,7 +645,7 @@ class InputOutput:
self.clipboard_watcher.start()
def get_continuation(width, line_number, is_soft_wrap):
return ". "
return self.prompt_prefix
line = self.prompt_session.prompt(
show,
@ -696,6 +804,9 @@ class InputOutput:
):
self.num_user_asks += 1
# Ring the bell if needed
self.ring_bell()
question_id = (question, subject)
if question_id in self.never_prompts:
@ -750,14 +861,19 @@ class InputOutput:
self.user_input(f"{question}{res}", log_only=False)
else:
while True:
if self.prompt_session:
res = self.prompt_session.prompt(
question,
style=style,
complete_while_typing=False,
)
else:
res = input(question)
try:
if self.prompt_session:
res = self.prompt_session.prompt(
question,
style=style,
complete_while_typing=False,
)
else:
res = input(question)
except EOFError:
# Treat EOF (Ctrl+D) as if the user pressed Enter
res = default
break
if not res:
res = default
@ -801,6 +917,9 @@ class InputOutput:
def prompt_ask(self, question, default="", subject=None):
self.num_user_asks += 1
# Ring the bell if needed
self.ring_bell()
if subject:
self.tool_output()
self.tool_output(subject, bold=True)
@ -812,15 +931,19 @@ class InputOutput:
elif self.yes is False:
res = "no"
else:
if self.prompt_session:
res = self.prompt_session.prompt(
question + " ",
default=default,
style=style,
complete_while_typing=True,
)
else:
res = input(question + " ")
try:
if self.prompt_session:
res = self.prompt_session.prompt(
question + " ",
default=default,
style=style,
complete_while_typing=True,
)
else:
res = input(question + " ")
except EOFError:
# Treat EOF (Ctrl+D) as if the user pressed Enter
res = default
hist = f"{question.strip()} {res.strip()}"
self.append_chat_history(hist, linebreak=True, blockquote=True)
@ -840,6 +963,7 @@ class InputOutput:
if not isinstance(message, Text):
message = Text(message)
color = ensure_hash_prefix(color) if color else None
style = dict(style=color) if self.pretty and color else dict()
try:
self.console.print(message, **style)
@ -870,7 +994,7 @@ class InputOutput:
style = dict()
if self.pretty:
if self.tool_output_color:
style["color"] = self.tool_output_color
style["color"] = ensure_hash_prefix(self.tool_output_color)
style["reverse"] = bold
style = RichStyle(**style)
@ -882,6 +1006,10 @@ class InputOutput:
return mdStream
def assistant_output(self, message, pretty=None):
if not message:
self.tool_warning("Empty response received from LLM. Check your provider account?")
return
show_resp = message
# Coder will force pretty off if fence is not triple-backticks
@ -893,7 +1021,7 @@ class InputOutput:
message, style=self.assistant_output_color, code_theme=self.code_theme
)
else:
show_resp = Text(message or "<no response>")
show_resp = Text(message or "(empty response)")
self.console.print(show_resp)
@ -904,6 +1032,61 @@ class InputOutput:
def print(self, message=""):
print(message)
def llm_started(self):
"""Mark that the LLM has started processing, so we should ring the bell on next input"""
self.bell_on_next_input = True
def get_default_notification_command(self):
"""Return a default notification command based on the operating system."""
import platform
system = platform.system()
if system == "Darwin": # macOS
# Check for terminal-notifier first
if shutil.which("terminal-notifier"):
return f"terminal-notifier -title 'Aider' -message '{NOTIFICATION_MESSAGE}'"
# Fall back to osascript
return (
f'osascript -e \'display notification "{NOTIFICATION_MESSAGE}" with title "Aider"\''
)
elif system == "Linux":
# Check for common Linux notification tools
for cmd in ["notify-send", "zenity"]:
if shutil.which(cmd):
if cmd == "notify-send":
return f"notify-send 'Aider' '{NOTIFICATION_MESSAGE}'"
elif cmd == "zenity":
return f"zenity --notification --text='{NOTIFICATION_MESSAGE}'"
return None # No known notification tool found
elif system == "Windows":
# PowerShell notification
return (
"powershell -command"
" \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');"
f" [System.Windows.Forms.MessageBox]::Show('{NOTIFICATION_MESSAGE}',"
" 'Aider')\""
)
return None # Unknown system
def ring_bell(self):
"""Ring the terminal bell if needed and clear the flag"""
if self.bell_on_next_input and self.notifications:
if self.notifications_command:
try:
result = subprocess.run(
self.notifications_command, shell=True, capture_output=True
)
if result.returncode != 0 and result.stderr:
error_msg = result.stderr.decode("utf-8", errors="replace")
self.tool_warning(f"Failed to run notifications command: {error_msg}")
except Exception as e:
self.tool_warning(f"Failed to run notifications command: {e}")
else:
print("\a", end="", flush=True) # Ring the bell
self.bell_on_next_input = False # Clear the flag
def toggle_multiline_mode(self):
"""Toggle between normal and multiline input modes"""
self.multiline_mode = not self.multiline_mode

View file

@ -4,6 +4,7 @@ import subprocess
import sys
import traceback
import warnings
import shlex
from dataclasses import dataclass
from pathlib import Path
@ -44,7 +45,7 @@ class Linter:
return fname
def run_cmd(self, cmd, rel_fname, code):
cmd += " " + rel_fname
cmd += " " + shlex.quote(rel_fname)
returncode = 0
stdout = ""

View file

@ -1,4 +1,3 @@
import configparser
import json
import os
import re
@ -25,11 +24,13 @@ from aider.coders import Coder
from aider.coders.base_coder import UnknownEditFormat
from aider.commands import Commands, SwitchCoder
from aider.copypaste import ClipboardWatcher
from aider.deprecated import handle_deprecated_model_args
from aider.format_settings import format_settings, scrub_sensitive_info
from aider.history import ChatSummary
from aider.io import InputOutput
from aider.llm import litellm # noqa: F401; properly init litellm on launch
from aider.models import ModelSettings
from aider.onboarding import offer_openrouter_oauth, select_default_model
from aider.repo import ANY_GIT_ERROR, GitRepo
from aider.report import report_uncaught_exceptions
from aider.versioncheck import check_version, install_from_main_branch, install_upgrade
@ -126,17 +127,15 @@ def setup_git(git_root, io):
if not repo:
return
user_name = None
user_email = None
with repo.config_reader() as config:
try:
user_name = config.get_value("user", "name", None)
except (configparser.NoSectionError, configparser.NoOptionError):
pass
try:
user_email = config.get_value("user", "email", None)
except (configparser.NoSectionError, configparser.NoOptionError):
pass
try:
user_name = repo.git.config("--get", "user.name") or None
except git.exc.GitCommandError:
user_name = None
try:
user_email = repo.git.config("--get", "user.email") or None
except git.exc.GitCommandError:
user_email = None
if user_name and user_email:
return repo.working_tree_dir
@ -214,18 +213,6 @@ def check_streamlit_install(io):
)
def install_tree_sitter_language_pack(io):
return utils.check_pip_install_extra(
io,
"tree_sitter_language_pack",
"Install tree_sitter_language_pack?",
[
"tree-sitter-language-pack==0.4.0",
"tree-sitter==0.24.0",
],
)
def write_streamlit_credentials():
from streamlit.file_util import get_streamlit_file_path
@ -371,11 +358,21 @@ def register_models(git_root, model_settings_fname, io, verbose=False):
def load_dotenv_files(git_root, dotenv_fname, encoding="utf-8"):
# Standard .env file search path
dotenv_files = generate_search_path_list(
".env",
git_root,
dotenv_fname,
)
# Explicitly add the OAuth keys file to the beginning of the list
oauth_keys_file = Path.home() / ".aider" / "oauth-keys.env"
if oauth_keys_file.exists():
# Insert at the beginning so it's loaded first (and potentially overridden)
dotenv_files.insert(0, str(oauth_keys_file.resolve()))
# Remove duplicates if it somehow got included by generate_search_path_list
dotenv_files = list(dict.fromkeys(dotenv_files))
loaded = []
for fname in dotenv_files:
try:
@ -519,6 +516,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
litellm._load_litellm()
litellm._lazy_module.client_session = httpx.Client(verify=False)
litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False)
# Set verify_ssl on the model_info_manager
models.model_info_manager.set_verify_ssl(False)
if args.timeout:
models.request_timeout = args.timeout
@ -567,6 +566,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
editingmode=editing_mode,
fancy_input=args.fancy_input,
multiline_mode=args.multiline,
notifications=args.notifications,
notifications_command=args.notifications_command,
)
io = get_io(args.pretty)
@ -606,6 +607,9 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if args.openai_api_key:
os.environ["OPENAI_API_KEY"] = args.openai_api_key
# Handle deprecated model shortcut args
handle_deprecated_model_args(args, io)
if args.openai_api_base:
os.environ["OPENAI_API_BASE"] = args.openai_api_base
if args.openai_api_version:
@ -718,19 +722,9 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
analytics.event("exit", reason="Upgrade completed")
return 0 if success else 1
if args.install_tree_sitter_language_pack:
success = install_tree_sitter_language_pack(io)
analytics.event("exit", reason="Install TSLP completed")
return 0 if success else 1
if args.check_update:
check_version(io, verbose=args.verbose)
if args.list_models:
models.print_matching_models(io, args.list_models)
analytics.event("exit", reason="Listed models")
return 0
if args.git:
git_root = setup_git(git_root, io)
if args.gitignore:
@ -750,6 +744,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
register_models(git_root, args.model_settings_file, io, verbose=args.verbose)
register_litellm_models(git_root, args.model_metadata_file, io, verbose=args.verbose)
if args.list_models:
models.print_matching_models(io, args.list_models)
analytics.event("exit", reason="Listed models")
return 0
# Process any command line aliases
if args.alias:
for alias_def in args.alias:
@ -763,26 +762,49 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
alias, model = parts
models.MODEL_ALIASES[alias.strip()] = model.strip()
if not args.model:
# Select model based on available API keys
model_key_pairs = [
("ANTHROPIC_API_KEY", "sonnet"),
("DEEPSEEK_API_KEY", "deepseek"),
("OPENROUTER_API_KEY", "openrouter/anthropic/claude-3.5-sonnet"),
("OPENAI_API_KEY", "gpt-4o"),
("GEMINI_API_KEY", "flash"),
]
selected_model_name = select_default_model(args, io, analytics)
if not selected_model_name:
# Error message and analytics event are handled within select_default_model
# It might have already offered OAuth if no model/keys were found.
# If it failed here, we exit.
return 1
args.model = selected_model_name # Update args with the selected model
for env_key, model_name in model_key_pairs:
if os.environ.get(env_key):
args.model = model_name
io.tool_warning(
f"Found {env_key} so using {model_name} since no --model was specified."
# Check if an OpenRouter model was selected/specified but the key is missing
if args.model.startswith("openrouter/") and not os.environ.get("OPENROUTER_API_KEY"):
io.tool_warning(
f"The specified model '{args.model}' requires an OpenRouter API key, which was not"
" found."
)
# Attempt OAuth flow because the specific model needs it
if offer_openrouter_oauth(io, analytics):
# OAuth succeeded, the key should now be in os.environ.
# Check if the key is now present after the flow.
if os.environ.get("OPENROUTER_API_KEY"):
io.tool_output(
"OpenRouter successfully connected."
) # Inform user connection worked
else:
# This case should ideally not happen if offer_openrouter_oauth succeeded
# but check defensively.
io.tool_error(
"OpenRouter authentication seemed successful, but the key is still missing."
)
break
if not args.model:
io.tool_error("You need to specify a --model and an --api-key to use.")
io.offer_url(urls.models_and_keys, "Open documentation url for more info?")
analytics.event(
"exit",
reason="OpenRouter key missing after successful OAuth for specified model",
)
return 1
else:
# OAuth failed or was declined by the user
io.tool_error(
f"Unable to proceed without an OpenRouter API key for model '{args.model}'."
)
io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
analytics.event(
"exit",
reason="OpenRouter key missing for specified model and OAuth failed/declined",
)
return 1
main_model = models.Model(
@ -790,18 +812,52 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
weak_model=args.weak_model,
editor_model=args.editor_model,
editor_edit_format=args.editor_edit_format,
verbose=args.verbose,
)
# add --reasoning-effort cli param
# Check if deprecated remove_reasoning is set
if main_model.remove_reasoning is not None:
io.tool_warning(
"Model setting 'remove_reasoning' is deprecated, please use 'reasoning_tag' instead."
)
# Set reasoning effort and thinking tokens if specified
if args.reasoning_effort is not None:
if not getattr(main_model, "extra_params", None):
main_model.extra_params = {}
if "extra_body" not in main_model.extra_params:
main_model.extra_params["extra_body"] = {}
main_model.extra_params["extra_body"]["reasoning_effort"] = args.reasoning_effort
# Apply if check is disabled or model explicitly supports it
if not args.check_model_accepts_settings or (
main_model.accepts_settings and "reasoning_effort" in main_model.accepts_settings
):
main_model.set_reasoning_effort(args.reasoning_effort)
if args.thinking_tokens is not None:
# Apply if check is disabled or model explicitly supports it
if not args.check_model_accepts_settings or (
main_model.accepts_settings and "thinking_tokens" in main_model.accepts_settings
):
main_model.set_thinking_tokens(args.thinking_tokens)
# Show warnings about unsupported settings that are being ignored
if args.check_model_accepts_settings:
settings_to_check = [
{"arg": args.reasoning_effort, "name": "reasoning_effort"},
{"arg": args.thinking_tokens, "name": "thinking_tokens"},
]
for setting in settings_to_check:
if setting["arg"] is not None and (
not main_model.accepts_settings
or setting["name"] not in main_model.accepts_settings
):
io.tool_warning(
f"Warning: {main_model.name} does not support '{setting['name']}', ignoring."
)
io.tool_output(
f"Use --no-check-model-accepts-settings to force the '{setting['name']}'"
" setting."
)
if args.copy_paste and args.edit_format is None:
if main_model.edit_format in ("diff", "whole"):
if main_model.edit_format in ("diff", "whole", "diff-fenced"):
main_model.edit_format = "editor-" + main_model.edit_format
if args.verbose:
@ -847,6 +903,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
attribute_commit_message_committer=args.attribute_commit_message_committer,
commit_prompt=args.commit_prompt,
subtree_only=args.subtree_only,
git_commit_verify=args.git_commit_verify,
)
except FileNotFoundError:
pass
@ -872,6 +929,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
parser=parser,
verbose=args.verbose,
editor=args.editor,
original_read_only_fnames=read_only_fnames,
)
summarizer = ChatSummary(
@ -894,6 +952,9 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
else:
map_tokens = args.map_tokens
# Track auto-commits configuration
analytics.event("auto_commits", enabled=bool(args.auto_commits))
try:
coder = Coder.create(
main_model=main_model,
@ -926,6 +987,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
chat_language=args.chat_language,
detect_urls=args.detect_urls,
auto_copy_context=args.copy_paste,
auto_accept_architect=args.auto_accept_architect,
)
except UnknownEditFormat as err:
io.tool_error(str(err))
@ -1039,6 +1101,9 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
io.tool_output(f"Cur working dir: {Path.cwd()}")
io.tool_output(f"Git working dir: {git_root}")
if args.stream and args.cache_prompts:
io.tool_warning("Cost estimates may be inaccurate when using streaming and caching.")
if args.load:
commands.cmd_load(args.load)
@ -1084,6 +1149,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
except SwitchCoder as switch:
coder.ok_to_warm_cache = False
# Set the placeholder if provided
if hasattr(switch, "placeholder") and switch.placeholder is not None:
io.placeholder = switch.placeholder
kwargs = dict(io=io, from_coder=coder)
kwargs.update(switch.kwargs)
if "show_announcements" in kwargs:

View file

@ -3,9 +3,12 @@
import io
import time
from rich import box
from rich.console import Console
from rich.live import Live
from rich.markdown import Markdown
from rich.markdown import CodeBlock, Heading, Markdown
from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text
from aider.dump import dump # noqa: F401
@ -46,6 +49,46 @@ The end.
""" # noqa: E501
class NoInsetCodeBlock(CodeBlock):
"""A code block with syntax highlighting and no padding."""
def __rich_console__(self, console, options):
code = str(self.text).rstrip()
syntax = Syntax(code, self.lexer_name, theme=self.theme, word_wrap=True, padding=(1, 0))
yield syntax
class LeftHeading(Heading):
"""A heading class that renders left-justified."""
def __rich_console__(self, console, options):
text = self.text
text.justify = "left" # Override justification
if self.tag == "h1":
# Draw a border around h1s, but keep text left-aligned
yield Panel(
text,
box=box.HEAVY,
style="markdown.h1.border",
)
else:
# Styled text for h2 and beyond
if self.tag == "h2":
yield Text("") # Keep the blank line before h2
yield text
class NoInsetMarkdown(Markdown):
"""Markdown with code blocks that have no padding and left-justified headings."""
elements = {
**Markdown.elements,
"fence": NoInsetCodeBlock,
"code_block": NoInsetCodeBlock,
"heading_open": LeftHeading,
}
class MarkdownStream:
"""Streaming markdown renderer that progressively displays content with a live updating window.
@ -88,7 +131,7 @@ class MarkdownStream:
# Render the markdown to a string buffer
string_io = io.StringIO()
console = Console(file=string_io, force_terminal=True)
markdown = Markdown(text, **self.mdargs)
markdown = NoInsetMarkdown(text, **self.mdargs)
console.print(markdown)
output = string_io.getvalue()
@ -186,6 +229,7 @@ if __name__ == "__main__":
_text = _text * 10
pm = MarkdownStream()
print("Using NoInsetMarkdown for code blocks with padding=0")
for i in range(6, len(_text), 5):
pm.update(_text[:i])
time.sleep(0.01)

View file

@ -5,7 +5,6 @@ import json
import math
import os
import platform
import re
import sys
import time
from dataclasses import dataclass, fields
@ -19,6 +18,7 @@ from PIL import Image
from aider.dump import dump # noqa: F401
from aider.llm import litellm
from aider.sendchat import ensure_alternating_roles, sanity_check_messages
from aider.utils import check_pip_install_extra
RETRY_TIMEOUT = 60
@ -88,8 +88,14 @@ MODEL_ALIASES = {
"3": "gpt-3.5-turbo",
# Other models
"deepseek": "deepseek/deepseek-chat",
"flash": "gemini/gemini-2.5-flash-preview-04-17",
"quasar": "openrouter/openrouter/quasar-alpha",
"r1": "deepseek/deepseek-reasoner",
"flash": "gemini/gemini-2.0-flash-exp",
"gemini-2.5-pro": "gemini/gemini-2.5-pro-exp-03-25",
"gemini": "gemini/gemini-2.5-pro-preview-03-25",
"gemini-exp": "gemini/gemini-2.5-pro-exp-03-25",
"grok3": "xai/grok-3-beta",
"optimus": "openrouter/openrouter/optimus-alpha",
}
# Model metadata loaded from resources and user's files.
@ -103,6 +109,7 @@ class ModelSettings:
use_repo_map: bool = False
send_undo_reply: bool = False
lazy: bool = False
overeager: bool = False
reminder: str = "user"
examples_as_sys_msg: bool = False
extra_params: Optional[dict] = None
@ -113,8 +120,10 @@ class ModelSettings:
streaming: bool = True
editor_model_name: Optional[str] = None
editor_edit_format: Optional[str] = None
remove_reasoning: Optional[str] = None
reasoning_tag: Optional[str] = None
remove_reasoning: Optional[str] = None # Deprecated alias for reasoning_tag
system_prompt_prefix: Optional[str] = None
accepts_settings: Optional[list] = None
# Load model settings from package resource
@ -137,23 +146,37 @@ class ModelInfoManager:
self.cache_file = self.cache_dir / "model_prices_and_context_window.json"
self.content = None
self.local_model_metadata = {}
self._load_cache()
self.verify_ssl = True
self._cache_loaded = False
def set_verify_ssl(self, verify_ssl):
self.verify_ssl = verify_ssl
def _load_cache(self):
if self._cache_loaded:
return
try:
self.cache_dir.mkdir(parents=True, exist_ok=True)
if self.cache_file.exists():
cache_age = time.time() - self.cache_file.stat().st_mtime
if cache_age < self.CACHE_TTL:
self.content = json.loads(self.cache_file.read_text())
try:
self.content = json.loads(self.cache_file.read_text())
except json.JSONDecodeError:
# If the cache file is corrupted, treat it as missing
self.content = None
except OSError:
pass
self._cache_loaded = True
def _update_cache(self):
try:
import requests
response = requests.get(self.MODEL_INFO_URL, timeout=5)
# Respect the --no-verify-ssl switch
response = requests.get(self.MODEL_INFO_URL, timeout=5, verify=self.verify_ssl)
if response.status_code == 200:
self.content = response.json()
try:
@ -173,6 +196,9 @@ class ModelInfoManager:
if data:
return data
# Ensure cache is loaded before checking content
self._load_cache()
if not self.content:
self._update_cache()
@ -212,11 +238,14 @@ model_info_manager = ModelInfoManager()
class Model(ModelSettings):
def __init__(self, model, weak_model=None, editor_model=None, editor_edit_format=None):
def __init__(
self, model, weak_model=None, editor_model=None, editor_edit_format=None, verbose=False
):
# Map any alias to its canonical name
model = MODEL_ALIASES.get(model, model)
self.name = model
self.verbose = verbose
self.max_chat_history_tokens = 1024
self.weak_model = None
@ -259,6 +288,11 @@ class Model(ModelSettings):
val = getattr(source, field.name)
setattr(self, field.name, val)
# Handle backward compatibility: if remove_reasoning is set but reasoning_tag isn't,
# use remove_reasoning's value for reasoning_tag
if self.reasoning_tag is None and self.remove_reasoning is not None:
self.reasoning_tag = self.remove_reasoning
def configure_model_settings(self, model):
# Look for exact model match
exact_match = False
@ -269,6 +303,10 @@ class Model(ModelSettings):
exact_match = True
break # Continue to apply overrides
# Initialize accepts_settings if it's None
if self.accepts_settings is None:
self.accepts_settings = []
model = model.lower()
# If no exact match, try generic settings
@ -276,7 +314,11 @@ class Model(ModelSettings):
self.apply_generic_model_settings(model)
# Apply override settings last if they exist
if self.extra_model_settings and self.extra_model_settings.extra_params:
if (
self.extra_model_settings
and self.extra_model_settings.extra_params
and self.extra_model_settings.name == "aider/extra_params"
):
# Initialize extra_params if it doesn't exist
if not self.extra_params:
self.extra_params = {}
@ -296,6 +338,23 @@ class Model(ModelSettings):
self.use_repo_map = True
self.use_temperature = False
self.system_prompt_prefix = "Formatting re-enabled. "
self.system_prompt_prefix = "Formatting re-enabled. "
if "reasoning_effort" not in self.accepts_settings:
self.accepts_settings.append("reasoning_effort")
return # <--
if "gpt-4.1-mini" in model:
self.edit_format = "diff"
self.use_repo_map = True
self.reminder = "sys"
self.examples_as_sys_msg = False
return # <--
if "gpt-4.1" in model:
self.edit_format = "diff"
self.use_repo_map = True
self.reminder = "sys"
self.examples_as_sys_msg = False
return # <--
if "/o1-mini" in model:
@ -317,6 +376,8 @@ class Model(ModelSettings):
self.use_temperature = False
self.streaming = False
self.system_prompt_prefix = "Formatting re-enabled. "
if "reasoning_effort" not in self.accepts_settings:
self.accepts_settings.append("reasoning_effort")
return # <--
if "deepseek" in model and "v3" in model:
@ -331,7 +392,7 @@ class Model(ModelSettings):
self.use_repo_map = True
self.examples_as_sys_msg = True
self.use_temperature = False
self.remove_reasoning = "think"
self.reasoning_tag = "think"
return # <--
if ("llama3" in model or "llama-3" in model) and "70b" in model:
@ -357,6 +418,15 @@ class Model(ModelSettings):
self.reminder = "sys"
return # <--
if "3-7-sonnet" in model:
self.edit_format = "diff"
self.use_repo_map = True
self.examples_as_sys_msg = True
self.reminder = "user"
if "thinking_tokens" not in self.accepts_settings:
self.accepts_settings.append("thinking_tokens")
return # <--
if "3.5-sonnet" in model or "3-5-sonnet" in model:
self.edit_format = "diff"
self.use_repo_map = True
@ -380,6 +450,16 @@ class Model(ModelSettings):
self.use_repo_map = True
return # <--
if "qwq" in model and "32b" in model and "preview" not in model:
self.edit_format = "diff"
self.editor_edit_format = "editor-diff"
self.use_repo_map = True
self.reasoning_tag = "think"
self.examples_as_sys_msg = True
self.use_temperature = 0.6
self.extra_params = dict(top_p=0.95)
return # <--
# use the defaults
if self.edit_format == "diff":
self.use_repo_map = True
@ -427,6 +507,8 @@ class Model(ModelSettings):
if not self.editor_edit_format:
self.editor_edit_format = self.editor_model.edit_format
if self.editor_edit_format in ("diff", "whole", "diff-fenced"):
self.editor_edit_format = "editor-" + self.editor_edit_format
return self.editor_model
@ -535,6 +617,21 @@ class Model(ModelSettings):
model = self.name
res = litellm.validate_environment(model)
# If missing AWS credential keys but AWS_PROFILE is set, consider AWS credentials valid
if res["missing_keys"] and any(
key in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] for key in res["missing_keys"]
):
if model.startswith("bedrock/") or model.startswith("us.anthropic."):
if os.environ.get("AWS_PROFILE"):
res["missing_keys"] = [
k
for k in res["missing_keys"]
if k not in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
]
if not res["missing_keys"]:
res["keys_in_environment"] = True
if res["keys_in_environment"]:
return res
if res["missing_keys"]:
@ -559,6 +656,108 @@ class Model(ModelSettings):
map_tokens = max(map_tokens, 1024)
return map_tokens
def set_reasoning_effort(self, effort):
"""Set the reasoning effort parameter for models that support it"""
if effort is not None:
if not self.extra_params:
self.extra_params = {}
if "extra_body" not in self.extra_params:
self.extra_params["extra_body"] = {}
self.extra_params["extra_body"]["reasoning_effort"] = effort
def parse_token_value(self, value):
"""
Parse a token value string into an integer.
Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
Args:
value: String or int token value
Returns:
Integer token value
"""
if isinstance(value, int):
return value
if not isinstance(value, str):
return int(value) # Try to convert to int
value = value.strip().upper()
if value.endswith("K"):
multiplier = 1024
value = value[:-1]
elif value.endswith("M"):
multiplier = 1024 * 1024
value = value[:-1]
else:
multiplier = 1
# Convert to float first to handle decimal values like "10.5k"
return int(float(value) * multiplier)
def set_thinking_tokens(self, value):
"""
Set the thinking token budget for models that support it.
Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
"""
if value is not None:
num_tokens = self.parse_token_value(value)
self.use_temperature = False
if not self.extra_params:
self.extra_params = {}
# OpenRouter models use 'reasoning' instead of 'thinking'
if self.name.startswith("openrouter/"):
self.extra_params["reasoning"] = {"max_tokens": num_tokens}
else:
self.extra_params["thinking"] = {"type": "enabled", "budget_tokens": num_tokens}
def get_raw_thinking_tokens(self):
"""Get formatted thinking token budget if available"""
budget = None
if self.extra_params:
# Check for OpenRouter reasoning format
if "reasoning" in self.extra_params and "max_tokens" in self.extra_params["reasoning"]:
budget = self.extra_params["reasoning"]["max_tokens"]
# Check for standard thinking format
elif (
"thinking" in self.extra_params and "budget_tokens" in self.extra_params["thinking"]
):
budget = self.extra_params["thinking"]["budget_tokens"]
return budget
def get_thinking_tokens(self):
budget = self.get_raw_thinking_tokens()
if budget is not None:
# Format as xx.yK for thousands, xx.yM for millions
if budget >= 1024 * 1024:
value = budget / (1024 * 1024)
if value == int(value):
return f"{int(value)}M"
else:
return f"{value:.1f}M"
else:
value = budget / 1024
if value == int(value):
return f"{int(value)}k"
else:
return f"{value:.1f}k"
return None
def get_reasoning_effort(self):
"""Get reasoning effort value if available"""
if (
self.extra_params
and "extra_body" in self.extra_params
and "reasoning_effort" in self.extra_params["extra_body"]
):
return self.extra_params["extra_body"]["reasoning_effort"]
return None
def is_deepseek_r1(self):
name = self.name.lower()
if "deepseek" not in name:
@ -577,7 +776,6 @@ class Model(ModelSettings):
kwargs = dict(
model=self.name,
messages=messages,
stream=stream,
)
@ -606,17 +804,13 @@ class Model(ModelSettings):
hash_object = hashlib.sha1(key)
if "timeout" not in kwargs:
kwargs["timeout"] = request_timeout
if self.verbose:
dump(kwargs)
kwargs["messages"] = messages
res = litellm.completion(**kwargs)
return hash_object, res
def remove_reasoning_content(self, res):
if not self.remove_reasoning:
return res
pattern = f"<{self.remove_reasoning}>.*?</{self.remove_reasoning}>"
res = re.sub(pattern, "", res, flags=re.DOTALL).strip()
return res
def simple_send_with_retries(self, messages):
from aider.exceptions import LiteLLMExceptions
@ -637,7 +831,9 @@ class Model(ModelSettings):
if not response or not hasattr(response, "choices") or not response.choices:
return None
res = response.choices[0].message.content
return self.remove_reasoning_content(res)
from aider.reasoning_tags import remove_reasoning_content
return remove_reasoning_content(res, self.reasoning_tag)
except litellm_ex.exceptions_tuple() as err:
ex_info = litellm_ex.get_ex_info(err)
@ -760,6 +956,9 @@ def sanity_check_model(io, model):
show = True
io.tool_warning(f"Warning for {model}: Unknown which environment variables are required.")
# Check for model-specific dependencies
check_for_dependencies(io, model.name)
if not model.info:
show = True
io.tool_warning(
@ -775,11 +974,38 @@ def sanity_check_model(io, model):
return show
def check_for_dependencies(io, model_name):
"""
Check for model-specific dependencies and install them if needed.
Args:
io: The IO object for user interaction
model_name: The name of the model to check dependencies for
"""
# Check if this is a Bedrock model and ensure boto3 is installed
if model_name.startswith("bedrock/"):
check_pip_install_extra(
io, "boto3", "AWS Bedrock models require the boto3 package.", ["boto3"]
)
# Check if this is a Vertex AI model and ensure google-cloud-aiplatform is installed
elif model_name.startswith("vertex_ai/"):
check_pip_install_extra(
io,
"google.cloud.aiplatform",
"Google Vertex AI models require the google-cloud-aiplatform package.",
["google-cloud-aiplatform"],
)
def fuzzy_match_models(name):
name = name.lower()
chat_models = set()
for orig_model, attrs in litellm.model_cost.items():
model_metadata = list(litellm.model_cost.items())
model_metadata += list(model_info_manager.local_model_metadata.items())
for orig_model, attrs in model_metadata:
model = orig_model.lower()
if attrs.get("mode") != "chat":
continue

428
aider/onboarding.py Normal file
View file

@ -0,0 +1,428 @@
import base64
import hashlib
import http.server
import os
import secrets
import socketserver
import threading
import time
import webbrowser
from urllib.parse import parse_qs, urlparse
import requests
from aider import urls
from aider.io import InputOutput
def check_openrouter_tier(api_key):
"""
Checks if the user is on a free tier for OpenRouter.
Args:
api_key: The OpenRouter API key to check.
Returns:
A boolean indicating if the user is on a free tier (True) or paid tier (False).
Returns True if the check fails.
"""
try:
response = requests.get(
"https://openrouter.ai/api/v1/auth/key",
headers={"Authorization": f"Bearer {api_key}"},
timeout=5, # Add a reasonable timeout
)
response.raise_for_status()
data = response.json()
# According to the documentation, 'is_free_tier' will be true if the user has never paid
return data.get("data", {}).get("is_free_tier", True) # Default to True if not found
except Exception:
# If there's any error, we'll default to assuming free tier
return True
def try_to_select_default_model():
"""
Attempts to select a default model based on available API keys.
Checks OpenRouter tier status to select appropriate model.
Returns:
The name of the selected model, or None if no suitable default is found.
"""
# Special handling for OpenRouter
openrouter_key = os.environ.get("OPENROUTER_API_KEY")
if openrouter_key:
# Check if the user is on a free tier
is_free_tier = check_openrouter_tier(openrouter_key)
if is_free_tier:
return "openrouter/google/gemini-2.5-pro-exp-03-25:free"
else:
return "openrouter/anthropic/claude-3.7-sonnet"
# Select model based on other available API keys
model_key_pairs = [
("ANTHROPIC_API_KEY", "sonnet"),
("DEEPSEEK_API_KEY", "deepseek"),
("OPENAI_API_KEY", "gpt-4o"),
("GEMINI_API_KEY", "gemini/gemini-2.5-pro-exp-03-25"),
("VERTEXAI_PROJECT", "vertex_ai/gemini-2.5-pro-exp-03-25"),
]
for env_key, model_name in model_key_pairs:
api_key_value = os.environ.get(env_key)
if api_key_value:
return model_name
return None
def offer_openrouter_oauth(io, analytics):
"""
Offers OpenRouter OAuth flow to the user if no API keys are found.
Args:
io: The InputOutput object for user interaction.
analytics: The Analytics object for tracking events.
Returns:
True if authentication was successful, False otherwise.
"""
# No API keys found - Offer OpenRouter OAuth
io.tool_output("OpenRouter provides free and paid access to many LLMs.")
# Use confirm_ask which handles non-interactive cases
if io.confirm_ask(
"Login to OpenRouter or create a free account?",
default="y",
):
analytics.event("oauth_flow_initiated", provider="openrouter")
openrouter_key = start_openrouter_oauth_flow(io, analytics)
if openrouter_key:
# Successfully got key via OAuth, use the default OpenRouter model
# Ensure OPENROUTER_API_KEY is now set in the environment for later use
os.environ["OPENROUTER_API_KEY"] = openrouter_key
# Track OAuth success leading to model selection
analytics.event("oauth_flow_success")
return True
# OAuth failed or was cancelled by user implicitly (e.g., closing browser)
# Error messages are handled within start_openrouter_oauth_flow
analytics.event("oauth_flow_failure")
io.tool_error("OpenRouter authentication did not complete successfully.")
# Fall through to the final error message
return False
def select_default_model(args, io, analytics):
"""
Selects a default model based on available API keys if no model is specified.
Offers OAuth flow for OpenRouter if no keys are found.
Args:
args: The command line arguments object.
io: The InputOutput object for user interaction.
analytics: The Analytics object for tracking events.
Returns:
The name of the selected model, or None if no suitable default is found.
"""
if args.model:
return args.model # Model already specified
model = try_to_select_default_model()
if model:
io.tool_warning(f"Using {model} model with API key from environment.")
analytics.event("auto_model_selection", model=model)
return model
no_model_msg = "No LLM model was specified and no API keys were provided."
io.tool_warning(no_model_msg)
# Try OAuth if no model was detected
offer_openrouter_oauth(io, analytics)
# Check again after potential OAuth success
model = try_to_select_default_model()
if model:
return model
io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
# Helper function to find an available port
def find_available_port(start_port=8484, end_port=8584):
for port in range(start_port, end_port + 1):
try:
# Check if the port is available by trying to bind to it
with socketserver.TCPServer(("localhost", port), None):
return port
except OSError:
# Port is likely already in use
continue
return None
# PKCE code generation
def generate_pkce_codes():
code_verifier = secrets.token_urlsafe(64)
hasher = hashlib.sha256()
hasher.update(code_verifier.encode("utf-8"))
code_challenge = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=").decode("utf-8")
return code_verifier, code_challenge
# Function to exchange the authorization code for an API key
def exchange_code_for_key(code, code_verifier, io):
try:
response = requests.post(
"https://openrouter.ai/api/v1/auth/keys",
headers={"Content-Type": "application/json"},
json={
"code": code,
"code_verifier": code_verifier,
"code_challenge_method": "S256",
},
timeout=30, # Add a timeout
)
response.raise_for_status() # Raise exception for bad status codes (4xx or 5xx)
data = response.json()
api_key = data.get("key")
if not api_key:
io.tool_error("Error: 'key' not found in OpenRouter response.")
io.tool_error(f"Response: {response.text}")
return None
return api_key
except requests.exceptions.Timeout:
io.tool_error("Error: Request to OpenRouter timed out during code exchange.")
return None
except requests.exceptions.HTTPError as e:
io.tool_error(
"Error exchanging code for OpenRouter key:"
f" {e.response.status_code} {e.response.reason}"
)
io.tool_error(f"Response: {e.response.text}")
return None
except requests.exceptions.RequestException as e:
io.tool_error(f"Error exchanging code for OpenRouter key: {e}")
return None
except Exception as e:
io.tool_error(f"Unexpected error during code exchange: {e}")
return None
# Function to start the OAuth flow
def start_openrouter_oauth_flow(io, analytics):
"""Initiates the OpenRouter OAuth PKCE flow using a local server."""
port = find_available_port()
if not port:
io.tool_error("Could not find an available port between 8484 and 8584.")
io.tool_error("Please ensure a port in this range is free, or configure manually.")
return None
callback_url = f"http://localhost:{port}/callback/aider"
auth_code = None
server_error = None
server_started = threading.Event()
shutdown_server = threading.Event()
class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
nonlocal auth_code, server_error
parsed_path = urlparse(self.path)
if parsed_path.path == "/callback/aider":
query_params = parse_qs(parsed_path.query)
if "code" in query_params:
auth_code = query_params["code"][0]
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(
b"<html><body><h1>Success!</h1>"
b"<p>Aider has received the authentication code. "
b"You can close this browser tab.</p></body></html>"
)
# Signal the main thread to shut down the server
# Signal the main thread to shut down the server
shutdown_server.set()
else:
# Redirect to aider website if 'code' is missing (e.g., user visited manually)
self.send_response(302) # Found (temporary redirect)
self.send_header("Location", urls.website)
self.end_headers()
# No need to set server_error, just redirect.
# Do NOT shut down the server here; wait for timeout or success.
else:
# Redirect anything else (e.g., favicon.ico) to the main website as well
self.send_response(302)
self.send_header("Location", urls.website)
self.end_headers()
self.wfile.write(b"Not Found")
def log_message(self, format, *args):
# Suppress server logging to keep terminal clean
pass
def run_server():
nonlocal server_error
try:
with socketserver.TCPServer(("localhost", port), OAuthCallbackHandler) as httpd:
io.tool_output(f"Temporary server listening on {callback_url}", log_only=True)
server_started.set() # Signal that the server is ready
# Wait until shutdown is requested or timeout occurs (handled by main thread)
while not shutdown_server.is_set():
httpd.handle_request() # Handle one request at a time
# Add a small sleep to prevent busy-waiting if needed,
# though handle_request should block appropriately.
time.sleep(0.1)
io.tool_output("Shutting down temporary server.", log_only=True)
except Exception as e:
server_error = f"Failed to start or run temporary server: {e}"
server_started.set() # Signal even if failed, error will be checked
shutdown_server.set() # Ensure shutdown logic proceeds
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
# Wait briefly for the server to start, or for an error
if not server_started.wait(timeout=5):
io.tool_error("Temporary authentication server failed to start in time.")
shutdown_server.set() # Ensure thread exits if it eventually starts
server_thread.join(timeout=1)
return None
# Check if server failed during startup
if server_error:
io.tool_error(server_error)
shutdown_server.set() # Ensure thread exits
server_thread.join(timeout=1)
return None
# Generate codes and URL
code_verifier, code_challenge = generate_pkce_codes()
auth_url_base = "https://openrouter.ai/auth"
auth_params = {
"callback_url": callback_url,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
}
auth_url = f"{auth_url_base}?{'&'.join(f'{k}={v}' for k, v in auth_params.items())}"
io.tool_output("\nPlease open this URL in your browser to connect Aider with OpenRouter:")
io.tool_output()
print(auth_url)
MINUTES = 5
io.tool_output(f"\nWaiting up to {MINUTES} minutes for you to finish in the browser...")
io.tool_output("Use Control-C to interrupt.")
try:
webbrowser.open(auth_url)
except Exception:
pass
# Wait for the callback to set the auth_code or for timeout/error
interrupted = False
try:
shutdown_server.wait(timeout=MINUTES * 60) # Convert minutes to seconds
except KeyboardInterrupt:
io.tool_warning("\nOAuth flow interrupted.")
analytics.event("oauth_flow_failed", provider="openrouter", reason="user_interrupt")
interrupted = True
# Ensure the server thread is signaled to shut down
shutdown_server.set()
# Join the server thread to ensure it's cleaned up
server_thread.join(timeout=1)
if interrupted:
return None # Return None if interrupted by user
if server_error:
io.tool_error(f"Authentication failed: {server_error}")
analytics.event("oauth_flow_failed", provider="openrouter", reason=server_error)
return None
if not auth_code:
io.tool_error("Authentication with OpenRouter failed.")
analytics.event("oauth_flow_failed", provider="openrouter")
return None
io.tool_output("Completing authentication...")
analytics.event("oauth_flow_code_received", provider="openrouter")
# Exchange code for key
api_key = exchange_code_for_key(auth_code, code_verifier, io)
if api_key:
# Set env var for the current session immediately
os.environ["OPENROUTER_API_KEY"] = api_key
# Save the key to the oauth-keys.env file
try:
config_dir = os.path.expanduser("~/.aider")
os.makedirs(config_dir, exist_ok=True)
key_file = os.path.join(config_dir, "oauth-keys.env")
with open(key_file, "a", encoding="utf-8") as f:
f.write(f'OPENROUTER_API_KEY="{api_key}"\n')
io.tool_warning("Aider will load the OpenRouter key automatically in future sessions.")
io.tool_output()
analytics.event("oauth_flow_success", provider="openrouter")
return api_key
except Exception as e:
io.tool_error(f"Successfully obtained key, but failed to save it to file: {e}")
io.tool_warning("Set OPENROUTER_API_KEY environment variable for this session only.")
# Still return the key for the current session even if saving failed
analytics.event("oauth_flow_save_failed", provider="openrouter", reason=str(e))
return api_key
else:
io.tool_error("Authentication with OpenRouter failed.")
analytics.event("oauth_flow_failed", provider="openrouter", reason="code_exchange_failed")
return None
# Dummy Analytics class for testing
class DummyAnalytics:
def event(self, *args, **kwargs):
# print(f"Analytics Event: {args} {kwargs}") # Optional: print events
pass
def main():
"""Main function to test the OpenRouter OAuth flow."""
print("Starting OpenRouter OAuth flow test...")
# Use a real IO object for interaction
io = InputOutput(
pretty=True,
yes=False,
input_history_file=None,
chat_history_file=None,
tool_output_color="BLUE",
tool_error_color="RED",
)
# Use a dummy analytics object
analytics = DummyAnalytics()
# Ensure OPENROUTER_API_KEY is not set, to trigger the flow naturally
# (though start_openrouter_oauth_flow doesn't check this itself)
if "OPENROUTER_API_KEY" in os.environ:
print("Warning: OPENROUTER_API_KEY is already set in environment.")
# del os.environ["OPENROUTER_API_KEY"] # Optionally unset it for testing
api_key = start_openrouter_oauth_flow(io, analytics)
if api_key:
print("\nOAuth flow completed successfully!")
print(f"Obtained API Key (first 5 chars): {api_key[:5]}...")
# Be careful printing the key, even partially
else:
print("\nOAuth flow failed or was cancelled.")
print("\nOpenRouter OAuth flow test finished.")
if __name__ == "__main__":
main()

View file

@ -15,7 +15,7 @@ Use these for <type>: fix, feat, build, chore, ci, docs, style, refactor, perf,
Ensure the commit message:
- Starts with the appropriate prefix.
- Is in the imperative mood (e.g., \"Add feature\" not \"Added feature\" or \"Adding feature\").
- Is in the imperative mood (e.g., \"add feature\" not \"added feature\" or \"adding feature\").
- Does not exceed 72 characters.
Reply only with the one-line commit message, without any additional text, explanations, \

View file

@ -0,0 +1,7 @@
These scm files are all adapted from the github repositories listed here:
https://github.com/Goldziher/tree-sitter-language-pack/blob/main/sources/language_definitions.json
See this URL for information on the licenses of each repo:
https://github.com/Goldziher/tree-sitter-language-pack/

View file

@ -0,0 +1,5 @@
(function_declarator
declarator: (identifier) @name.definition.function) @definition.function
(call_expression
function: (identifier) @name.reference.call) @reference.call

View file

@ -0,0 +1,9 @@
(struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
(declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class
(function_declarator declarator: (identifier) @name.definition.function) @definition.function
(type_definition declarator: (type_identifier) @name.definition.type) @definition.type
(enum_specifier name: (type_identifier) @name.definition.type) @definition.type

View file

@ -0,0 +1,16 @@
; Definitions
(intent_def
(intent) @name.definition.intent) @definition.intent
(slot_def
(slot) @name.definition.slot) @definition.slot
(alias_def
(alias) @name.definition.alias) @definition.alias
; References
(slot_ref
(slot) @name.reference.slot) @reference.slot
(alias_ref
(alias) @name.reference.alias) @reference.alias

View file

@ -0,0 +1,122 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Function Definitions ;;;;;;;;;;;;;;;;;;;;;;;
(defun_header
function_name: (sym_lit) @name.definition.function) @definition.function
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Function Calls ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Basically, we consider every list literal with symbol as the
;;; first element to be a call to a function named by that element.
;;; But we must exclude some cases. Note, tree-sitter @ignore
;;; cases only work if they are declared before the cases
;;; we want to include.
;; Exclude lambda lists for function definitions
;; For example:
;;
;; (defun my-func (arg1 arg2) ...)
;;
;; do not treat (arg1 arg2) as a call of function arg1
;;
(defun_header
lambda_list: (list_lit . [(sym_lit) (package_lit)] @ignore))
;; Similar to the above, but for
;;
;; (defmethod m ((type1 param1) (type2 param2)) ...)
;;
;; where list literals having symbol as their first element
;; are nested inside the lambda list.
(defun_header
lambda_list: (list_lit (list_lit . [(sym_lit) (package_lit)] @ignore)))
;;
;; (let ((var ...) (var2 ...)) ...)
;;
;; - exclude var, var2
;; - the same for let*, flet, labels, macrolet, symbol-macrolet
(list_lit . [(sym_lit) (package_lit)] @name.reference.call
. (list_lit (list_lit . [(sym_lit) (package_lit)] @ignore))
(#match? @name.reference.call
"(?i)^(cl:)?(let|let\\*|flet|labels|macrolet|symbol-macrolet)$")
)
;; TODO:
;; - exclude also:
;; - (defclass name (parent parent2)
;; ((slot1 ...)
;; (slot2 ...))
;; exclude the parent, slot1, slot2
;; - (flet ((func-1 (param1 param2))) ...)
;; - we already exclude func-1, but param1 is still recognized
;; as a function call - exclude it too
;; - the same for labels
;; - the same macrolet
;; - what else?
;; (that's a non-goal to completely support all macros
;; and special operators, but every one we support
;; makes the solution a little bit better)
;; - (flet ((func-1 (param1 param2))) ...)
;; - instead of simply excluding it, as we do today,
;; tag func-1 as @local.definition.function (I suppose)
;; - the same for labels, macrolet
;; - @local.scope for let, let*, flet, labels, macrolet
;; - I guess the whole span of the scope text,
;; till the closing paren, should be tagged as @local.scope;
;; Hopefully, combined with @local.definition.function
;; within the scope, the usual @reference.call within
;; that scope will refer to the local definition,
;; and there will be no need to use @local.reference.call
;; (which is more difficult to implement).
;; - When implementing, remember the scope rules differences
;; of let vs let*, flet vs labels.
;; Include all other cases - list literal with symbol as the
;; first element
(list_lit . [(sym_lit) (package_lit)] @name.reference.call) @reference.call
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; classes
(list_lit . [(sym_lit) (package_lit)] @ignore
. [(sym_lit) (package_lit)] @name.definition.class
(#match? @ignore "(?i)^(cl:)?defclass$")
) @definition.class
(list_lit . [(sym_lit) (package_lit)] @ignore
. (quoting_lit [(sym_lit) (package_lit)] @name.reference.class)
(#match? @ignore "(?i)^(cl:)?make-instance$")
) @reference.class
;;; TODO:
;; - @reference.class for base classes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; TODO:
;; - Symbols referenced in defpackage
;;
;; (defpackage ...
;; (:export (symbol-a :symbol-b #:symbol-c "SYMBOL-D")))
;;
;; The goal is to allow quick navigation from the API
;; overview in the form of defpackage, to the definition
;; where user can read parameters, docstring, etc.
;; - The @name must not include the colon, or sharpsign colon, quotes,
;; just symbol-a, symbol-b, symbol-c, sybmol-d
;; - Downcase the names specified as string literals?
;; ("SYMBOL-D" -> symbol-d)
;; - We don't know if the exported symbol is a function, variable,
;; class or something else. The official doc
;; (https://tree-sitter.github.io/tree-sitter/code-navigation-systems)
;; does not even suggest a tag for variable reference.
;; (Although in practice, the `tree-sitter tags` command
;; allows any @reference.* and @definition.* tags)
;; Probably it's better to just use @reference.call for all
;; the symbols in the :export clause.
;;
;; - The same for the export function call:
;;
;; (export '(symbol-a :symbol-b #:symbol-c "SYMBOL-D"))

View file

@ -0,0 +1,15 @@
(struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
(declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class
(function_declarator declarator: (identifier) @name.definition.function) @definition.function
(function_declarator declarator: (field_identifier) @name.definition.function) @definition.function
(function_declarator declarator: (qualified_identifier scope: (namespace_identifier) @local.scope name: (identifier) @name.definition.method)) @definition.method
(type_definition declarator: (type_identifier) @name.definition.type) @definition.type
(enum_specifier name: (type_identifier) @name.definition.type) @definition.type
(class_specifier name: (type_identifier) @name.definition.class) @definition.class

View file

@ -0,0 +1,26 @@
; Based on https://github.com/tree-sitter/tree-sitter-c-sharp/blob/master/queries/tags.scm
; MIT License.
(class_declaration name: (identifier) @name.definition.class) @definition.class
(class_declaration (base_list (_) @name.reference.class)) @reference.class
(interface_declaration name: (identifier) @name.definition.interface) @definition.interface
(interface_declaration (base_list (_) @name.reference.interface)) @reference.interface
(method_declaration name: (identifier) @name.definition.method) @definition.method
(object_creation_expression type: (identifier) @name.reference.class) @reference.class
(type_parameter_constraints_clause (identifier) @name.reference.class) @reference.class
(type_parameter_constraint (type type: (identifier) @name.reference.class)) @reference.class
(variable_declaration type: (identifier) @name.reference.class) @reference.class
(invocation_expression function: (member_access_expression name: (identifier) @name.reference.send)) @reference.send
(namespace_declaration name: (identifier) @name.definition.module) @definition.module
(namespace_declaration name: (identifier) @name.definition.module) @module

View file

@ -0,0 +1,26 @@
(module_def (module_declaration (module_fqn) @name.definition.module)) @definition.module
(struct_declaration (struct) . (identifier) @name.definition.class) @definition.class
(interface_declaration (interface) . (identifier) @name.definition.interface) @definition.interface
(enum_declaration (enum) . (identifier) @name.definition.type) @definition.type
(class_declaration (class) . (identifier) @name.definition.class) @definition.class
(constructor (this) @name.definition.method) @definition.method
(destructor (this) @name.definition.method) @definition.method
(postblit (this) @name.definition.method) @definition.method
(manifest_declarator . (identifier) @name.definition.type) @definition.type
(function_declaration (identifier) @name.definition.function) @definition.function
(union_declaration (union) . (identifier) @name.definition.type) @definition.type
(anonymous_enum_declaration (enum_member . (identifier) @name.definition.constant)) @definition.constant
(enum_declaration (enum_member . (identifier) @name.definition.constant)) @definition.constant
(call_expression (identifier) @name.reference.call) @reference.call
(call_expression (type (template_instance (identifier) @name.reference.call))) @reference.call
(parameter (type (identifier) @name.reference.class) @reference.class (identifier))
(variable_declaration (type (identifier) @name.reference.class) @reference.class (declarator))

View file

@ -0,0 +1,92 @@
(class_definition
name: (identifier) @name.definition.class) @definition.class
(method_signature
(function_signature)) @definition.method
(type_alias
(type_identifier) @name.definition.type) @definition.type
(method_signature
(getter_signature
name: (identifier) @name.definition.method)) @definition.method
(method_signature
(setter_signature
name: (identifier) @name.definition.method)) @definition.method
(method_signature
(function_signature
name: (identifier) @name.definition.method)) @definition.method
(method_signature
(factory_constructor_signature
(identifier) @name.definition.method)) @definition.method
(method_signature
(constructor_signature
name: (identifier) @name.definition.method)) @definition.method
(method_signature
(operator_signature)) @definition.method
(method_signature) @definition.method
(mixin_declaration
(mixin)
(identifier) @name.definition.mixin) @definition.mixin
(extension_declaration
name: (identifier) @name.definition.extension) @definition.extension
(new_expression
(type_identifier) @name.reference.class) @reference.class
(enum_declaration
name: (identifier) @name.definition.enum) @definition.enum
(function_signature
name: (identifier) @name.definition.function) @definition.function
(initialized_variable_definition
name: (identifier)
value: (identifier) @name.reference.class
value: (selector
"!"?
(argument_part
(arguments
(argument)*))?)?) @reference.class
(assignment_expression
left: (assignable_expression
(identifier)
(unconditional_assignable_selector
"."
(identifier) @name.reference.send))) @reference.call
(assignment_expression
left: (assignable_expression
(identifier)
(conditional_assignable_selector
"?."
(identifier) @name.reference.send))) @reference.call
((identifier) @name.reference.send
(selector
"!"?
(conditional_assignable_selector
"?." (identifier) @name.reference.send)?
(unconditional_assignable_selector
"."? (identifier) @name.reference.send)?
(argument_part
(arguments
(argument)*))?)*
(cascade_section
(cascade_selector
(identifier)) @name.reference.send
(argument_part
(arguments
(argument)*))?)?) @reference.call

View file

@ -0,0 +1,5 @@
;; defun/defsubst
(function_definition name: (symbol) @name.definition.function) @definition.function
;; Treat macros as function definitions for the sake of TAGS.
(macro_definition name: (symbol) @name.definition.function) @definition.function

View file

@ -0,0 +1,54 @@
; Definitions
; * modules and protocols
(call
target: (identifier) @ignore
(arguments (alias) @name.definition.module)
(#any-of? @ignore "defmodule" "defprotocol")) @definition.module
; * functions/macros
(call
target: (identifier) @ignore
(arguments
[
; zero-arity functions with no parentheses
(identifier) @name.definition.function
; regular function clause
(call target: (identifier) @name.definition.function)
; function clause with a guard clause
(binary_operator
left: (call target: (identifier) @name.definition.function)
operator: "when")
])
(#any-of? @ignore "def" "defp" "defdelegate" "defguard" "defguardp" "defmacro" "defmacrop" "defn" "defnp")) @definition.function
; References
; ignore calls to kernel/special-forms keywords
(call
target: (identifier) @ignore
(#any-of? @ignore "def" "defp" "defdelegate" "defguard" "defguardp" "defmacro" "defmacrop" "defn" "defnp" "defmodule" "defprotocol" "defimpl" "defstruct" "defexception" "defoverridable" "alias" "case" "cond" "else" "for" "if" "import" "quote" "raise" "receive" "require" "reraise" "super" "throw" "try" "unless" "unquote" "unquote_splicing" "use" "with"))
; ignore module attributes
(unary_operator
operator: "@"
operand: (call
target: (identifier) @ignore))
; * function call
(call
target: [
; local
(identifier) @name.reference.call
; remote
(dot
right: (identifier) @name.reference.call)
]) @reference.call
; * pipe into function call
(binary_operator
operator: "|>"
right: (identifier) @name.reference.call) @reference.call
; * modules
(alias) @name.reference.module @reference.module

View file

@ -0,0 +1,19 @@
(value_declaration (function_declaration_left (lower_case_identifier) @name.definition.function)) @definition.function
(function_call_expr (value_expr (value_qid) @name.reference.function)) @reference.function
(exposed_value (lower_case_identifier) @name.reference.function) @reference.function
(type_annotation ((lower_case_identifier) @name.reference.function) (colon)) @reference.function
(type_declaration ((upper_case_identifier) @name.definition.type) ) @definition.type
(type_ref (upper_case_qid (upper_case_identifier) @name.reference.type)) @reference.type
(exposed_type (upper_case_identifier) @name.reference.type) @reference.type
(type_declaration (union_variant (upper_case_identifier) @name.definition.union)) @definition.union
(value_expr (upper_case_qid (upper_case_identifier) @name.reference.union)) @reference.union
(module_declaration
(upper_case_qid (upper_case_identifier)) @name.definition.module
) @definition.module

View file

@ -0,0 +1,41 @@
; Modules
(module) @name.reference.module @reference.module
(import alias: (identifier) @name.reference.module) @reference.module
(remote_type_identifier
module: (identifier) @name.reference.module) @reference.module
((field_access
record: (identifier) @name.reference.module)
(#is-not? local)) @reference.module
; Functions
(function
name: (identifier) @name.definition.function) @definition.function
(external_function
name: (identifier) @name.definition.function) @definition.function
(unqualified_import (identifier) @name.reference.function) @reference.function
((function_call
function: (identifier) @name.reference.function) @reference.function
(#is-not? local))
((field_access
record: (identifier) @ignore
field: (label) @name.reference.function)
(#is-not? local)) @reference.function
((binary_expression
operator: "|>"
right: (identifier) @name.reference.function)
(#is-not? local)) @reference.function
; Types
(type_definition
(type_name
name: (type_identifier) @name.definition.type)) @definition.type
(type_definition
(data_constructors
(data_constructor
name: (constructor_name) @name.definition.constructor))) @definition.constructor
(external_type
(type_name
name: (type_identifier) @name.definition.type)) @definition.type
(type_identifier) @name.reference.type @reference.type
(constructor_name) @name.reference.constructor @reference.constructor

View file

@ -0,0 +1,42 @@
(
(comment)* @doc
.
(function_declaration
name: (identifier) @name.definition.function) @definition.function
(#strip! @doc "^//\\s*")
(#set-adjacent! @doc @definition.function)
)
(
(comment)* @doc
.
(method_declaration
name: (field_identifier) @name.definition.method) @definition.method
(#strip! @doc "^//\\s*")
(#set-adjacent! @doc @definition.method)
)
(call_expression
function: [
(identifier) @name.reference.call
(parenthesized_expression (identifier) @name.reference.call)
(selector_expression field: (field_identifier) @name.reference.call)
(parenthesized_expression (selector_expression field: (field_identifier) @name.reference.call))
]) @reference.call
(type_spec
name: (type_identifier) @name.definition.type) @definition.type
(type_identifier) @name.reference.type @reference.type
(package_clause "package" (package_identifier) @name.definition.module)
(type_declaration (type_spec name: (type_identifier) @name.definition.interface type: (interface_type)))
(type_declaration (type_spec name: (type_identifier) @name.definition.class type: (struct_type)))
(import_declaration (import_spec) @name.reference.module)
(var_declaration (var_spec name: (identifier) @name.definition.variable))
(const_declaration (const_spec name: (identifier) @name.definition.constant))

View file

@ -0,0 +1,20 @@
(class_declaration
name: (identifier) @name.definition.class) @definition.class
(method_declaration
name: (identifier) @name.definition.method) @definition.method
(method_invocation
name: (identifier) @name.reference.method
arguments: (argument_list) @reference.call)
(interface_declaration
name: (identifier) @name.definition.interface) @definition.interface
(type_list
(type_identifier) @name.reference.interface) @reference.implementation
(object_creation_expression
type: (type_identifier) @name.reference.class) @reference.class
(superclass (type_identifier) @name.reference.class) @reference.class

View file

@ -0,0 +1,34 @@
(function_declaration
name: [
(identifier) @name.definition.function
(dot_index_expression
field: (identifier) @name.definition.function)
]) @definition.function
(function_declaration
name: (method_index_expression
method: (identifier) @name.definition.method)) @definition.method
(assignment_statement
(variable_list .
name: [
(identifier) @name.definition.function
(dot_index_expression
field: (identifier) @name.definition.function)
])
(expression_list .
value: (function_definition))) @definition.function
(table_constructor
(field
name: (identifier) @name.definition.function
value: (function_definition))) @definition.function
(function_call
name: [
(identifier) @name.reference.call
(dot_index_expression
field: (identifier) @name.reference.call)
(method_index_expression
method: (identifier) @name.reference.method)
]) @reference.call

View file

@ -0,0 +1,39 @@
;Class definitions @definition.class
;Function definitions @definition.function
;Interface definitions @definition.interface
;Method definitions @definition.method
;Module definitions @definition.module
;Function/method calls @reference.call
;Class reference @reference.class
;Interface implementation @reference.implementation
(
(identifier) @reference.class
(#match? @reference.class "^_*[A-Z][a-zA-Z0-9_]*$")
)
(class_definition (identifier) @name.definition.class) @definition.class
(actor_definition (identifier) @name.definition.class) @definition.class
(primitive_definition (identifier) @name.definition.class) @definition.class
(struct_definition (identifier) @name.definition.class) @definition.class
(type_alias (identifier) @name.definition.class) @definition.class
(trait_definition (identifier) @name.definition.interface) @definition.interface
(interface_definition (identifier) @name.definition.interface) @definition.interface
(constructor (identifier) @name.definition.method) @definition.method
(method (identifier) @name.definition.method) @definition.method
(behavior (identifier) @name.definition.method) @definition.method
(class_definition (type) @name.reference.implementation) @reference.implementation
(actor_definition (type) @name.reference.implementation) @reference.implementation
(primitive_definition (type) @name.reference.implementation) @reference.implementation
(struct_definition (type) @name.reference.implementation) @reference.implementation
(type_alias (type) @name.reference.implementation) @reference.implementation
; calls - not catching all possible call cases of callees for capturing the method name
(call_expression callee: [(identifier) (ffi_identifier)] @name.reference.call) @reference.call
(call_expression callee: (generic_expression [(identifier) (ffi_identifier)] @name.reference.call)) @reference.call
(call_expression callee: (member_expression (identifier) @name.reference.call .)) @reference.call
(call_expression callee: (member_expression (generic_expression [(identifier) (ffi_identifier)] @name.reference.call) .)) @reference.call
; TODO: add more possible callee expressions
(call_expression) @reference.call

View file

@ -0,0 +1,5 @@
(property
(key) @name.definition.property) @definition.property
(substitution
(key) @name.reference.property) @reference.property

View file

@ -0,0 +1,14 @@
(module (expression_statement (assignment left: (identifier) @name.definition.constant) @definition.constant))
(class_definition
name: (identifier) @name.definition.class) @definition.class
(function_definition
name: (identifier) @name.definition.function) @definition.function
(call
function: [
(identifier) @name.reference.call
(attribute
attribute: (identifier) @name.reference.call)
]) @reference.call

View file

@ -0,0 +1,21 @@
(binary_operator
lhs: (identifier) @name.definition.function
operator: "<-"
rhs: (function_definition)
) @definition.function
(binary_operator
lhs: (identifier) @name.definition.function
operator: "="
rhs: (function_definition)
) @definition.function
(call
function: (identifier) @name.reference.call
) @reference.call
(call
function: (namespace_operator
rhs: (identifier) @name.reference.call
)
) @reference.call

View file

@ -0,0 +1,12 @@
(list
.
(symbol) @reference._define
(#match? @reference._define "^(define|define/contract)$")
.
(list
.
(symbol) @name.definition.function) @definition.function)
(list
.
(symbol) @name.reference.call)

View file

@ -0,0 +1,64 @@
; Method definitions
(
(comment)* @doc
.
[
(method
name: (_) @name.definition.method) @definition.method
(singleton_method
name: (_) @name.definition.method) @definition.method
]
(#strip! @doc "^#\\s*")
(#select-adjacent! @doc @definition.method)
)
(alias
name: (_) @name.definition.method) @definition.method
(setter
(identifier) @ignore)
; Class definitions
(
(comment)* @doc
.
[
(class
name: [
(constant) @name.definition.class
(scope_resolution
name: (_) @name.definition.class)
]) @definition.class
(singleton_class
value: [
(constant) @name.definition.class
(scope_resolution
name: (_) @name.definition.class)
]) @definition.class
]
(#strip! @doc "^#\\s*")
(#select-adjacent! @doc @definition.class)
)
; Module definitions
(
(module
name: [
(constant) @name.definition.module
(scope_resolution
name: (_) @name.definition.module)
]) @definition.module
)
; Calls
(call method: (identifier) @name.reference.call) @reference.call
(
[(identifier) (constant)] @name.reference.call @reference.call
(#is-not? local)
(#not-match? @name.reference.call "^(lambda|load|require|require_relative|__FILE__|__LINE__)$")
)

View file

@ -0,0 +1,60 @@
; ADT definitions
(struct_item
name: (type_identifier) @name.definition.class) @definition.class
(enum_item
name: (type_identifier) @name.definition.class) @definition.class
(union_item
name: (type_identifier) @name.definition.class) @definition.class
; type aliases
(type_item
name: (type_identifier) @name.definition.class) @definition.class
; method definitions
(declaration_list
(function_item
name: (identifier) @name.definition.method) @definition.method)
; function definitions
(function_item
name: (identifier) @name.definition.function) @definition.function
; trait definitions
(trait_item
name: (type_identifier) @name.definition.interface) @definition.interface
; module definitions
(mod_item
name: (identifier) @name.definition.module) @definition.module
; macro definitions
(macro_definition
name: (identifier) @name.definition.macro) @definition.macro
; references
(call_expression
function: (identifier) @name.reference.call) @reference.call
(call_expression
function: (field_expression
field: (field_identifier) @name.reference.call)) @reference.call
(macro_invocation
macro: (identifier) @name.reference.call) @reference.call
; implementations
(impl_item
trait: (type_identifier) @name.reference.implementation) @reference.implementation
(impl_item
type: (type_identifier) @name.reference.implementation
!trait) @reference.implementation

View file

@ -0,0 +1,43 @@
;; Method and Function declarations
(contract_declaration (_
(function_definition
name: (identifier) @name.definition.function) @definition.method))
(source_file
(function_definition
name: (identifier) @name.definition.function) @definition.function)
;; Contract, struct, enum and interface declarations
(contract_declaration
name: (identifier) @name.definition.class) @definition.class
(interface_declaration
name: (identifier) @name.definition.interface) @definition.interface
(library_declaration
name: (identifier) @name.definition.class) @definition.interface
(struct_declaration name: (identifier) @name.definition.class) @definition.class
(enum_declaration name: (identifier) @name.definition.class) @definition.class
(event_definition name: (identifier) @name.definition.class) @definition.class
;; Function calls
(call_expression (expression (identifier)) @name.reference.call ) @reference.call
(call_expression
(expression (member_expression
property: (_) @name.reference.method ))) @reference.call
;; Log emit
(emit_statement name: (_) @name.reference.class) @reference.class
;; Inheritance
(inheritance_specifier
ancestor: (user_defined_type (_) @name.reference.class . )) @reference.class
;; Imports ( note that unknown is not standardised )
(import_directive
import_name: (_) @name.reference.module ) @reference.unknown

View file

@ -0,0 +1,51 @@
(class_declaration
name: (type_identifier) @name.definition.class) @definition.class
(protocol_declaration
name: (type_identifier) @name.definition.interface) @definition.interface
(class_declaration
(class_body
[
(function_declaration
name: (simple_identifier) @name.definition.method
)
(subscript_declaration
(parameter (simple_identifier) @name.definition.method)
)
(init_declaration "init" @name.definition.method)
(deinit_declaration "deinit" @name.definition.method)
]
)
) @definition.method
(protocol_declaration
(protocol_body
[
(protocol_function_declaration
name: (simple_identifier) @name.definition.method
)
(subscript_declaration
(parameter (simple_identifier) @name.definition.method)
)
(init_declaration "init" @name.definition.method)
]
)
) @definition.method
(class_declaration
(class_body
[
(property_declaration
(pattern (simple_identifier) @name.definition.property)
)
]
)
) @definition.property
(property_declaration
(pattern (simple_identifier) @name.definition.property)
) @definition.property
(function_declaration
name: (simple_identifier) @name.definition.function) @definition.function

View file

@ -0,0 +1,20 @@
(assignment
key: "LABEL"
(value
(content) @name.definition.label)) @definition.label
(assignment
key: "GOTO"
(value
(content) @name.reference.label)) @reference.label
(assignment
key: "ENV"
(env_var) @name.definition.variable) @definition.variable
(match
key: "ENV"
(env_var) @name.reference.variable) @reference.variable
(var_sub
(env_var) @name.reference.variable) @reference.variable

View file

@ -0,0 +1,65 @@
; Definitions
(package_clause
name: (package_identifier) @name.definition.module) @definition.module
(trait_definition
name: (identifier) @name.definition.interface) @definition.interface
(enum_definition
name: (identifier) @name.definition.enum) @definition.enum
(simple_enum_case
name: (identifier) @name.definition.class) @definition.class
(full_enum_case
name: (identifier) @name.definition.class) @definition.class
(class_definition
name: (identifier) @name.definition.class) @definition.class
(object_definition
name: (identifier) @name.definition.object) @definition.object
(function_definition
name: (identifier) @name.definition.function) @definition.function
(val_definition
pattern: (identifier) @name.definition.variable) @definition.variable
(given_definition
name: (identifier) @name.definition.variable) @definition.variable
(var_definition
pattern: (identifier) @name.definition.variable) @definition.variable
(val_declaration
name: (identifier) @name.definition.variable) @definition.variable
(var_declaration
name: (identifier) @name.definition.variable) @definition.variable
(type_definition
name: (type_identifier) @name.definition.type) @definition.type
(class_parameter
name: (identifier) @name.definition.property) @definition.property
; References
(call_expression
(identifier) @name.reference.call) @reference.call
(instance_expression
(type_identifier) @name.reference.interface) @reference.interface
(instance_expression
(generic_type
(type_identifier) @name.reference.interface)) @reference.interface
(extends_clause
(type_identifier) @name.reference.class) @reference.class
(extends_clause
(generic_type
(type_identifier) @name.reference.class)) @reference.class

82
aider/reasoning_tags.py Normal file
View file

@ -0,0 +1,82 @@
#!/usr/bin/env python
import re
from aider.dump import dump # noqa
# Standard tag identifier
REASONING_TAG = "thinking-content-" + "7bbeb8e1441453ad999a0bbba8a46d4b"
# Output formatting
REASONING_START = "--------------\n► **THINKING**"
REASONING_END = "------------\n► **ANSWER**"
def remove_reasoning_content(res, reasoning_tag):
"""
Remove reasoning content from text based on tags.
Args:
res (str): The text to process
reasoning_tag (str): The tag name to remove
Returns:
str: Text with reasoning content removed
"""
if not reasoning_tag:
return res
# Try to match the complete tag pattern first
pattern = f"<{reasoning_tag}>.*?</{reasoning_tag}>"
res = re.sub(pattern, "", res, flags=re.DOTALL).strip()
# If closing tag exists but opening tag might be missing, remove everything before closing
# tag
closing_tag = f"</{reasoning_tag}>"
if closing_tag in res:
# Split on the closing tag and keep everything after it
parts = res.split(closing_tag, 1)
res = parts[1].strip() if len(parts) > 1 else res
return res
def replace_reasoning_tags(text, tag_name):
"""
Replace opening and closing reasoning tags with standard formatting.
Ensures exactly one blank line before START and END markers.
Args:
text (str): The text containing the tags
tag_name (str): The name of the tag to replace
Returns:
str: Text with reasoning tags replaced with standard format
"""
if not text:
return text
# Replace opening tag with proper spacing
text = re.sub(f"\\s*<{tag_name}>\\s*", f"\n{REASONING_START}\n\n", text)
# Replace closing tag with proper spacing
text = re.sub(f"\\s*</{tag_name}>\\s*", f"\n\n{REASONING_END}\n\n", text)
return text
def format_reasoning_content(reasoning_content, tag_name):
"""
Format reasoning content with appropriate tags.
Args:
reasoning_content (str): The content to format
tag_name (str): The tag name to use
Returns:
str: Formatted reasoning content with tags
"""
if not reasoning_content:
return ""
formatted = f"<{tag_name}>\n\n{reasoning_content}\n\n</{tag_name}>"
return formatted

View file

@ -9,6 +9,7 @@ try:
git.exc.ODBError,
git.exc.GitError,
git.exc.InvalidGitRepositoryError,
git.exc.GitCommandNotFound,
]
except ImportError:
git = None
@ -56,6 +57,7 @@ class GitRepo:
attribute_commit_message_committer=False,
commit_prompt=None,
subtree_only=False,
git_commit_verify=True,
):
self.io = io
self.models = models
@ -69,6 +71,7 @@ class GitRepo:
self.attribute_commit_message_committer = attribute_commit_message_committer
self.commit_prompt = commit_prompt
self.subtree_only = subtree_only
self.git_commit_verify = git_commit_verify
self.ignore_file_cache = {}
if git_dname:
@ -133,7 +136,9 @@ class GitRepo:
# if context:
# full_commit_message += "\n\n# Aider chat conversation:\n\n" + context
cmd = ["-m", full_commit_message, "--no-verify"]
cmd = ["-m", full_commit_message]
if not self.git_commit_verify:
cmd.append("--no-verify")
if fnames:
fnames = [str(self.abs_root_path(fn)) for fn in fnames]
for fname in fnames:
@ -145,7 +150,7 @@ class GitRepo:
else:
cmd += ["-a"]
original_user_name = self.repo.config_reader().get_value("user", "name")
original_user_name = self.repo.git.config("--get", "user.name")
original_committer_name_env = os.environ.get("GIT_COMMITTER_NAME")
committer_name = f"{original_user_name} (aider)"
@ -289,13 +294,19 @@ class GitRepo:
else:
try:
iterator = commit.tree.traverse()
blob = None # Initialize blob
while True:
try:
blob = next(iterator)
if blob.type == "blob": # blob is a file
files.add(blob.path)
except IndexError:
self.io.tool_warning(f"GitRepo: read error skipping {blob.path}")
# Handle potential index error during tree traversal
# without relying on potentially unassigned 'blob'
self.io.tool_warning(
"GitRepo: Index error encountered while reading git tree object."
" Skipping."
)
continue
except StopIteration:
break
@ -309,8 +320,11 @@ class GitRepo:
# Add staged files
index = self.repo.index
staged_files = [path for path, _ in index.entries.keys()]
files.update(self.normalize_path(path) for path in staged_files)
try:
staged_files = [path for path, _ in index.entries.keys()]
files.update(self.normalize_path(path) for path in staged_files)
except ANY_GIT_ERROR as err:
self.io.tool_error(f"Unable to read staged files: {err}")
res = [fname for fname in files if not self.ignored_file(fname)]

View file

@ -398,13 +398,30 @@ class RepoMap:
# dump(fname)
rel_fname = self.get_rel_fname(fname)
current_pers = 0.0 # Start with 0 personalization score
if fname in chat_fnames:
personalization[rel_fname] = personalize
current_pers += personalize
chat_rel_fnames.add(rel_fname)
if rel_fname in mentioned_fnames:
personalization[rel_fname] = personalize
# Use max to avoid double counting if in chat_fnames and mentioned_fnames
current_pers = max(current_pers, personalize)
# Check path components against mentioned_idents
path_obj = Path(rel_fname)
path_components = set(path_obj.parts)
basename_with_ext = path_obj.name
basename_without_ext, _ = os.path.splitext(basename_with_ext)
components_to_check = path_components.union({basename_with_ext, basename_without_ext})
matched_idents = components_to_check.intersection(mentioned_idents)
if matched_idents:
# Add personalization *once* if any path component matches a mentioned ident
current_pers += personalize
if current_pers > 0:
personalization[rel_fname] = current_pers # Assign the final calculated value
tags = list(self.get_tags(fname, rel_fname))
if tags is None:
@ -431,17 +448,33 @@ class RepoMap:
G = nx.MultiDiGraph()
# Add a small self-edge for every definition that has no references
# Helps with tree-sitter 0.23.2 with ruby, where "def greet(name)"
# isn't counted as a def AND a ref. tree-sitter 0.24.0 does.
for ident in defines.keys():
if ident in references:
continue
for definer in defines[ident]:
G.add_edge(definer, definer, weight=0.1, ident=ident)
for ident in idents:
if progress:
progress()
definers = defines[ident]
mul = 1.0
is_snake = ("_" in ident) and any(c.isalpha() for c in ident)
is_camel = any(c.isupper() for c in ident) and any(c.islower() for c in ident)
if ident in mentioned_idents:
mul = 10
elif ident.startswith("_"):
mul = 0.1
else:
mul = 1
mul *= 10
if (is_snake or is_camel) and len(ident) >= 8:
mul *= 10
if ident.startswith("_"):
mul *= 0.1
if len(defines[ident]) > 5:
mul *= 0.1
for referencer, num_refs in Counter(references[ident]).items():
for definer in definers:
@ -449,10 +482,14 @@ class RepoMap:
# if referencer == definer:
# continue
use_mul = mul
if referencer in chat_rel_fnames:
use_mul *= 50
# scale down so high freq (low value) mentions don't dominate
num_refs = math.sqrt(num_refs)
G.add_edge(referencer, definer, weight=mul * num_refs, ident=ident)
G.add_edge(referencer, definer, weight=use_mul * num_refs, ident=ident)
if not references:
pass

View file

@ -15,22 +15,6 @@
//"supports_tool_choice": true,
"supports_prompt_caching": true
},
"openrouter/deepseek/deepseek-r1": {
"max_tokens": 8192,
"max_input_tokens": 64000,
"max_output_tokens": 8192,
"input_cost_per_token": 0.00000055,
"input_cost_per_token_cache_hit": 0.00000014,
"cache_read_input_token_cost": 0.00000014,
"cache_creation_input_token_cost": 0.0,
"output_cost_per_token": 0.00000219,
"litellm_provider": "openrouter",
"mode": "chat",
//"supports_function_calling": true,
"supports_assistant_prefill": true,
//"supports_tool_choice": true,
"supports_prompt_caching": true
},
"openrouter/deepseek/deepseek-r1:free": {
"max_tokens": 8192,
"max_input_tokens": 64000,
@ -47,6 +31,49 @@
//"supports_tool_choice": true,
"supports_prompt_caching": true
},
"openrouter/deepseek/deepseek-chat:free": {
"max_tokens": 8192,
"max_input_tokens": 64000,
"max_output_tokens": 8192,
"input_cost_per_token": 0.0,
"input_cost_per_token_cache_hit": 0.0,
"cache_read_input_token_cost": 0.00,
"cache_creation_input_token_cost": 0.0,
"output_cost_per_token": 0.0,
"litellm_provider": "openrouter",
"mode": "chat",
//"supports_function_calling": true,
"supports_assistant_prefill": true,
//"supports_tool_choice": true,
"supports_prompt_caching": true
},
"openrouter/deepseek/deepseek-chat-v3-0324": {
"max_tokens": 8192,
"max_input_tokens": 64000,
"max_output_tokens": 8192,
"input_cost_per_token": 0.00000055,
"input_cost_per_token_cache_hit": 0.00000014,
"cache_read_input_token_cost": 0.00000014,
"cache_creation_input_token_cost": 0.0,
"output_cost_per_token": 0.00000219,
"litellm_provider": "openrouter",
"mode": "chat",
//"supports_function_calling": true,
"supports_assistant_prefill": true,
//"supports_tool_choice": true,
"supports_prompt_caching": true
},
"openrouter/deepseek/deepseek-chat-v3-0324:free": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 0,
"output_cost_per_token": 0,
"litellm_provider": "openrouter",
"supports_prompt_caching": true,
"mode": "chat",
"supports_tool_choice": true
},
"fireworks_ai/accounts/fireworks/models/deepseek-r1": {
"max_tokens": 160000,
"max_input_tokens": 128000,
@ -56,8 +83,8 @@
"output_cost_per_token": 0.000008,
"mode": "chat",
},
"fireworks_ai/accounts/fireworks/models/deepseek-v3": {
"max_tokens": 128000,
"fireworks_ai/accounts/fireworks/models/deepseek-v3-0324": {
"max_tokens": 160000,
"max_input_tokens": 100000,
"max_output_tokens": 8192,
"litellm_provider": "fireworks_ai",
@ -65,53 +92,25 @@
"output_cost_per_token": 0.0000009,
"mode": "chat",
},
"o3-mini": {
"max_tokens": 100000,
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"input_cost_per_token": 0.0000011,
"output_cost_per_token": 0.0000044,
"cache_read_input_token_cost": 0.00000055,
"litellm_provider": "openai",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_system_messages": true,
"supports_response_schema": true
},
"openrouter/openai/o3-mini": {
"max_tokens": 100000,
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"input_cost_per_token": 0.0000011,
"output_cost_per_token": 0.0000044,
"cache_read_input_token_cost": 0.00000055,
"openrouter/openrouter/quasar-alpha": {
"max_input_tokens": 1000000,
"max_output_tokens": 32000,
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"litellm_provider": "openrouter",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_function_calling": true,
"supports_system_messages": true,
"supports_response_schema": true
"supports_prompt_caching": true
},
"openrouter/openai/o3-mini-high": {
"max_tokens": 100000,
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"input_cost_per_token": 0.0000011,
"output_cost_per_token": 0.0000044,
"cache_read_input_token_cost": 0.00000055,
"openrouter/openrouter/optimus-alpha": {
"max_input_tokens": 1000000,
"max_output_tokens": 32000,
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"litellm_provider": "openrouter",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_system_messages": true,
"supports_response_schema": true
"mode": "chat"
},
"openrouter/openai/gpt-4o-mini": {
"max_tokens": 16384,
@ -131,26 +130,6 @@
"supports_prompt_caching": true,
"supports_system_messages": true
},
"claude-3-7-sonnet-20250219": {
"max_tokens": 8192,
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"input_cost_per_token": 0.000003,
"output_cost_per_token": 0.000015,
"cache_creation_input_token_cost": 0.00000375,
"cache_read_input_token_cost": 0.0000003,
"litellm_provider": "anthropic",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159,
"supports_assistant_prefill": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"deprecation_date": "2025-10-01",
"supports_tool_choice": true
},
"anthropic/claude-3-7-sonnet-20250219": {
"max_tokens": 8192,
"max_input_tokens": 200000,
@ -171,43 +150,6 @@
"deprecation_date": "2025-10-01",
"supports_tool_choice": true
},
"openrouter/anthropic/claude-3.7-sonnet": {
"max_tokens": 8192,
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"input_cost_per_token": 0.000003,
"output_cost_per_token": 0.000015,
"cache_creation_input_token_cost": 0.00000375,
"cache_read_input_token_cost": 0.0000003,
"litellm_provider": "openrouter",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159,
"supports_assistant_prefill": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"deprecation_date": "2025-10-01",
"supports_tool_choice": true
},
"gpt-4.5-preview": {
"max_tokens": 16384,
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"input_cost_per_token": 0.000075,
"output_cost_per_token": 0.00015,
"cache_read_input_token_cost": 0.0000375,
"litellm_provider": "openai",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"openai/gpt-4.5-preview": {
"max_tokens": 16384,
"max_input_tokens": 128000,
@ -225,4 +167,240 @@
"supports_system_messages": true,
"supports_tool_choice": true
},
"gemini/gemini-2.5-pro-exp-03-25": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 64000,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
//"litellm_provider": "vertex_ai-language-models",
"litellm_provider": "gemini",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true,
"supports_pdf_input": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
},
"vertex_ai/gemini-2.5-pro-exp-03-25": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 64000,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
"litellm_provider": "vertex_ai-language-models",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true,
"supports_pdf_input": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
},
"vertex_ai/gemini-2.5-pro-preview-03-25": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 64000,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0.00000125,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0.000010,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
"litellm_provider": "vertex_ai-language-models",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true,
"supports_pdf_input": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
},
"openrouter/google/gemini-2.5-pro-preview-03-25": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 64000,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0.00000125,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0.000010,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
"litellm_provider": "vertex_ai-language-models",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true,
"supports_pdf_input": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
},
"openrouter/google/gemini-2.5-pro-exp-03-25:free": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 64000,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
"litellm_provider": "openrouter",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_audio_input": true,
"supports_video_input": true,
"supports_pdf_input": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
},
"openrouter/x-ai/grok-3-beta": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 0.000003,
"output_cost_per_token": 0.000015,
"litellm_provider": "openrouter",
"mode": "chat"
},
"openrouter/x-ai/grok-3-mini-beta": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 0.0000003,
"output_cost_per_token": 0.0000005,
"litellm_provider": "openrouter",
"mode": "chat"
},
"openrouter/x-ai/grok-3-fast-beta": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 0.000005,
"output_cost_per_token": 0.000025,
"litellm_provider": "openrouter",
"mode": "chat"
},
"openrouter/x-ai/grok-3-mini-fast-beta": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 0.0000006,
"output_cost_per_token": 0.000004,
"litellm_provider": "openrouter",
"mode": "chat"
},
"openrouter/google/gemini-2.0-flash-exp:free": {
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 8192,
"max_images_per_prompt": 3000,
"max_videos_per_prompt": 10,
"max_video_length": 1,
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"max_pdf_size_mb": 30,
"litellm_provider": "openrouter",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": true,
"supports_vision": true,
"supports_response_schema": true,
"supports_audio_output": true,
"supports_tool_choice": true
},
}

View file

@ -185,6 +185,7 @@
editor_edit_format: editor-diff
- name: anthropic/claude-3-7-sonnet-20250219
overeager: true
edit_format: diff
weak_model_name: anthropic/claude-3-5-haiku-20241022
use_repo_map: true
@ -196,8 +197,10 @@
cache_control: true
editor_model_name: anthropic/claude-3-7-sonnet-20250219
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: anthropic/claude-3-7-sonnet-latest
overeager: true
edit_format: diff
weak_model_name: anthropic/claude-3-5-haiku-20241022
use_repo_map: true
@ -209,6 +212,7 @@
cache_control: true
editor_model_name: anthropic/claude-3-7-sonnet-latest
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: claude-3-7-sonnet-20250219
edit_format: diff
@ -222,8 +226,10 @@
cache_control: true
editor_model_name: claude-3-7-sonnet-20250219
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: claude-3-7-sonnet-latest
overeager: true
edit_format: diff
weak_model_name: claude-3-5-haiku-20241022
use_repo_map: true
@ -235,8 +241,10 @@
cache_control: true
editor_model_name: claude-3-7-sonnet-latest
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0
overeager: true
edit_format: diff
weak_model_name: bedrock/anthropic.claude-3-5-haiku-20241022-v1:0
use_repo_map: true
@ -248,8 +256,10 @@
cache_control: true
editor_model_name: bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0
overeager: true
edit_format: diff
weak_model_name: bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0
use_repo_map: true
@ -261,8 +271,10 @@
cache_control: true
editor_model_name: bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: bedrock_converse/anthropic.claude-3-7-sonnet-20250219-v1:0
overeager: true
edit_format: diff
weak_model_name: bedrock_converse/anthropic.claude-3-5-haiku-20241022-v1:0
use_repo_map: true
@ -274,8 +286,10 @@
cache_control: true
editor_model_name: bedrock_converse/anthropic.claude-3-7-sonnet-20250219-v1:0
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: bedrock_converse/us.anthropic.claude-3-7-sonnet-20250219-v1:0
overeager: true
edit_format: diff
weak_model_name: bedrock_converse/us.anthropic.claude-3-5-haiku-20241022-v1:0
use_repo_map: true
@ -287,8 +301,10 @@
cache_control: true
editor_model_name: bedrock_converse/us.anthropic.claude-3-7-sonnet-20250219-v1:0
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: vertex_ai/claude-3-7-sonnet@20250219
overeager: true
edit_format: diff
weak_model_name: vertex_ai/claude-3-5-haiku@20241022
use_repo_map: true
@ -297,8 +313,10 @@
max_tokens: 64000
editor_model_name: vertex_ai/claude-3-7-sonnet@20250219
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: vertex_ai-anthropic_models/vertex_ai/claude-3-7-sonnet@20250219
overeager: true
edit_format: diff
weak_model_name: vertex_ai/claude-3-5-haiku@20241022
use_repo_map: true
@ -307,8 +325,10 @@
max_tokens: 64000
editor_model_name: vertex_ai-anthropic_models/vertex_ai/claude-3-7-sonnet@20250219
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: openrouter/anthropic/claude-3.7-sonnet
overeager: true
edit_format: diff
weak_model_name: openrouter/anthropic/claude-3-5-haiku
use_repo_map: true
@ -320,8 +340,10 @@
cache_control: true
editor_model_name: openrouter/anthropic/claude-3.7-sonnet
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: openrouter/anthropic/claude-3.7-sonnet:beta
overeager: true
edit_format: diff
weak_model_name: openrouter/anthropic/claude-3-5-haiku
use_repo_map: true
@ -333,6 +355,7 @@
cache_control: true
editor_model_name: openrouter/anthropic/claude-3.7-sonnet
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
- name: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
edit_format: diff
@ -547,8 +570,8 @@
examples_as_sys_msg: true
extra_params:
max_tokens: 8192
include_reasoning: true
caches_by_default: true
use_temperature: false
editor_model_name: openrouter/deepseek/deepseek-chat
editor_edit_format: editor-diff
@ -560,6 +583,16 @@
extra_params:
max_tokens: 8192
caches_by_default: true
- name: openrouter/deepseek/deepseek-chat-v3-0324:free
edit_format: diff
weak_model_name: openrouter/deepseek/deepseek-chat-v3-0324:free
use_repo_map: true
examples_as_sys_msg: true
caches_by_default: true
use_temperature: false
editor_model_name: openrouter/deepseek/deepseek-chat-v3-0324:free
editor_edit_format: editor-diff
use_temperature: false
editor_model_name: openrouter/deepseek/deepseek-r1:free
editor_edit_format: editor-diff
@ -585,6 +618,18 @@
max_tokens: 8192
caches_by_default: true
- name: openrouter/deepseek/deepseek-chat:free
edit_format: diff
weak_model_name: openrouter/deepseek/deepseek-chat:free
use_repo_map: true
examples_as_sys_msg: true
extra_params:
max_tokens: 8192
caches_by_default: true
use_temperature: false
editor_model_name: openrouter/deepseek/deepseek-chat:free
editor_edit_format: editor-diff
- name: deepseek/deepseek-coder
edit_format: diff
use_repo_map: true
@ -623,6 +668,15 @@
reminder: sys
examples_as_sys_msg: true
- name: openrouter/deepseek/deepseek-chat-v3-0324
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: true
extra_params:
max_tokens: 8192
caches_by_default: true
- name: openrouter/openai/gpt-4o
edit_format: diff
weak_model_name: openrouter/openai/gpt-4o-mini
@ -682,6 +736,7 @@
streaming: false
editor_model_name: azure/gpt-4o
editor_edit_format: editor-diff
accepts_settings: ["reasoning_effort"]
- name: o1-preview
edit_format: architect
@ -720,6 +775,7 @@
editor_model_name: openrouter/openai/gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: openai/o1
edit_format: diff
@ -730,6 +786,7 @@
editor_model_name: openai/gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: o1
edit_format: diff
@ -740,6 +797,7 @@
editor_model_name: gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: openrouter/qwen/qwen-2.5-coder-32b-instruct
edit_format: diff
@ -759,7 +817,7 @@
use_temperature: false
editor_model_name: openrouter/deepseek/deepseek-chat
editor_edit_format: editor-diff
- name: fireworks_ai/accounts/fireworks/models/deepseek-r1
edit_format: diff
weak_model_name: fireworks_ai/accounts/fireworks/models/deepseek-v3
@ -768,7 +826,7 @@
streaming: true
editor_model_name: fireworks_ai/accounts/fireworks/models/deepseek-v3
editor_edit_format: editor-diff
remove_reasoning: think
reasoning_tag: think
extra_params:
max_tokens: 160000
@ -780,6 +838,14 @@
extra_params:
max_tokens: 128000
- name: fireworks_ai/accounts/fireworks/models/deepseek-v3-0324
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: true
extra_params:
max_tokens: 160000
- name: openai/o3-mini
edit_format: diff
weak_model_name: gpt-4o-mini
@ -788,7 +854,8 @@
editor_model_name: gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: o3-mini
edit_format: diff
weak_model_name: gpt-4o-mini
@ -797,6 +864,7 @@
editor_model_name: gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: openrouter/openai/o3-mini
edit_format: diff
@ -806,6 +874,7 @@
editor_model_name: openrouter/openai/gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: openrouter/openai/o3-mini-high
edit_format: diff
@ -815,6 +884,7 @@
editor_model_name: openrouter/openai/gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: azure/o3-mini
edit_format: diff
@ -824,6 +894,7 @@
editor_model_name: azure/gpt-4o
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
- name: gpt-4.5-preview
edit_format: diff
@ -834,7 +905,7 @@
examples_as_sys_msg: true
editor_model_name: gpt-4o
editor_edit_format: editor-diff
- name: openai/gpt-4.5-preview
edit_format: diff
weak_model_name: gpt-4o-mini
@ -844,4 +915,474 @@
examples_as_sys_msg: true
editor_model_name: openai/gpt-4o
editor_edit_format: editor-diff
- name: fireworks_ai/accounts/fireworks/models/qwq-32b
reasoning_tag: think
edit_format: diff
weak_model_name: fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct
use_repo_map: true
editor_model_name: fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct
editor_edit_format: editor-diff
reminder: user
examples_as_sys_msg: true
use_temperature: 0.6
extra_params:
max_tokens: 32000
top_p: 0.95
- name: groq/qwen-qwq-32b
reasoning_tag: think
edit_format: diff
weak_model_name: groq/qwen-2.5-coder-32b
use_repo_map: true
editor_model_name: groq/qwen-2.5-coder-32b
editor_edit_format: editor-diff
use_temperature: 0.6
extra_params:
max_tokens: 128000
top_p: 0.95
- name: cohere_chat/command-a-03-2025
examples_as_sys_msg: true
- name: openrouter/cohere/command-a-03-2025
examples_as_sys_msg: true
- name: gemini/gemma-3-27b-it
use_system_prompt: false
- name: openrouter/google/gemma-3-27b-it:free
use_system_prompt: false
- name: openrouter/google/gemma-3-27b-it
use_system_prompt: false
- name: gemini/gemini-2.5-pro-preview-03-25
overeager: true
edit_format: diff-fenced
use_repo_map: true
weak_model_name: gemini/gemini-2.0-flash
- name: gemini/gemini-2.5-pro-exp-03-25
edit_format: diff-fenced
use_repo_map: true
overeager: true
weak_model_name: gemini/gemini-2.5-flash-preview-04-17
- name: openrouter/google/gemini-2.5-pro-exp-03-25:free
edit_format: diff-fenced
overeager: true
use_repo_map: true
weak_model_name: openrouter/google/gemini-2.0-flash-exp:free
- name: vertex_ai/gemini-2.5-pro-exp-03-25
edit_format: diff-fenced
use_repo_map: true
weak_model_name: vertex_ai-language-models/gemini-2.5-flash-preview-04-17
overeager: true
editor_model_name: vertex_ai-language-models/gemini-2.5-flash-preview-04-17
- name: vertex_ai/gemini-2.5-pro-preview-03-25
edit_format: diff-fenced
use_repo_map: true
weak_model_name: vertex_ai-language-models/gemini-2.5-flash-preview-04-17
overeager: true
editor_model_name: vertex_ai-language-models/gemini-2.5-flash-preview-04-17
- name: openrouter/openrouter/quasar-alpha
use_repo_map: true
edit_format: diff
examples_as_sys_msg: true
- name: openrouter/x-ai/grok-3-beta
use_repo_map: true
edit_format: diff
- name: xai/grok-3-beta
use_repo_map: true
edit_format: diff
- name: openrouter/x-ai/grok-3-mini-beta
use_repo_map: true
edit_format: whole
accepts_settings:
- reasoning_effort
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o4-mini
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openrouter/openai/o4-mini
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: azure/o4-mini
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: xai/grok-3-mini-beta
use_repo_map: true
edit_format: whole
accepts_settings:
- reasoning_effort
#extra_params:
# extra_body:
# reasoning_effort: low
- name: openrouter/x-ai/grok-3-fast-beta
use_repo_map: true
edit_format: diff
- name: xai/grok-3-fast-beta
use_repo_map: true
edit_format: diff
- name: openrouter/x-ai/grok-3-mini-fast-beta
use_repo_map: true
edit_format: whole
accepts_settings:
- reasoning_effort
- name: xai/grok-3-mini-fast-beta
use_repo_map: true
edit_format: whole
accepts_settings:
- reasoning_effort
- name: openrouter/openrouter/optimus-alpha
use_repo_map: true
edit_format: diff
examples_as_sys_msg: true
- name: gpt-4.1
edit_format: diff
weak_model_name: gpt-4.1-mini
use_repo_map: true
reminder: sys # user: 52.x%/96.9%
examples_as_sys_msg: false # true: 51.6% correct, 95.6% well formed; false: 52.4%/98.2%
editor_model_name: gpt-4.1-mini
- name: openai/gpt-4.1
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
editor_model_name: openai/gpt-4.1-mini
- name: azure/gpt-4.1
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
editor_model_name: azure/gpt-4.1-mini
- name: openrouter/openai/gpt-4.1
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
editor_model_name: openrouter/openai/gpt-4.1-mini
- name: gpt-4.1-mini
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: false # false: 32.x%/92.4% (60+ malformed responses); true: 31.7/90.2/60+
- name: openai/gpt-4.1-mini
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
- name: azure/gpt-4.1-mini
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
- name: openrouter/openai/gpt-4.1-mini
edit_format: diff
use_repo_map: true
reminder: sys
examples_as_sys_msg: false
- name: o3
streaming: false
edit_format: diff
weak_model_name: gpt-4.1-mini
use_repo_map: true
editor_model_name: gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o4-mini
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openrouter/openai/o4-mini
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: azure/o4-mini
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o3
streaming: false
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o4-mini
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openrouter/openai/o4-mini
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: azure/o4-mini
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openrouter/openai/o3
streaming: false
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o4-mini
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openrouter/openai/o4-mini
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: azure/o4-mini
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: azure/o3
streaming: false
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: openai/o4-mini
edit_format: diff
weak_model_name: openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
- name: openrouter/openai/o4-mini
edit_format: diff
weak_model_name: openrouter/openai/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: openrouter/openai/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
- name: azure/o4-mini
edit_format: diff
weak_model_name: azure/gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: azure/gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
- name: o4-mini
edit_format: diff
weak_model_name: gpt-4.1-mini
use_repo_map: true
use_temperature: false
editor_model_name: gpt-4.1
editor_edit_format: editor-diff
system_prompt_prefix: "Formatting re-enabled. "
accepts_settings: ["reasoning_effort"]
examples_as_sys_msg: true
#extra_params:
# extra_body:
# reasoning_effort: high
- name: gemini/gemini-2.5-flash-preview-04-17
edit_format: diff
use_repo_map: true
accepts_settings: ["thinking_tokens"]
- name: gemini-2.5-flash-preview-04-17
edit_format: diff
use_repo_map: true
accepts_settings: ["thinking_tokens"]
- name: vertex_ai-language-models/gemini-2.5-flash-preview-04-17
edit_format: diff
use_repo_map: true
accepts_settings: ["thinking_tokens"]

View file

@ -159,7 +159,8 @@ class Scraper:
try:
response = page.goto(url, wait_until="networkidle", timeout=5000)
except PlaywrightTimeoutError:
self.print_error(f"Timeout while loading {url}")
print(f"Page didn't quiesce, scraping content anyway: {url}")
response = None
except PlaywrightError as e:
self.print_error(f"Error navigating to {url}: {str(e)}")
return None, None

View file

@ -308,7 +308,11 @@ def find_common_root(abs_fnames):
except OSError:
pass
return safe_abs_path(os.getcwd())
try:
return safe_abs_path(os.getcwd())
except FileNotFoundError:
# Fallback if cwd is deleted
return "."
def format_tokens(count):

View file

@ -64,7 +64,7 @@ class FileWatcher:
"""Watches source files for changes and AI comments"""
# Compiled regex pattern for AI comments
ai_comment_pattern = re.compile(r"(?:#|//|--) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE)
ai_comment_pattern = re.compile(r"(?:#|//|--|;+) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE)
def __init__(self, coder, gitignores=None, verbose=False, analytics=None, root=None):
self.coder = coder
@ -140,7 +140,10 @@ class FileWatcher:
roots_to_watch = self.get_roots_to_watch()
for changes in watch(
*roots_to_watch, watch_filter=self.filter_func, stop_event=self.stop_event
*roots_to_watch,
watch_filter=self.filter_func,
stop_event=self.stop_event,
ignore_permission_denied=True,
):
if self.handle_changes(changes):
return
@ -259,7 +262,7 @@ class FileWatcher:
line_nums.append(i)
comments.append(comment)
comment = comment.lower()
comment = comment.lstrip("/#-")
comment = comment.lstrip("/#-;") # Added semicolon for Lisp comments
comment = comment.strip()
if comment.startswith("ai!") or comment.endswith("ai!"):
has_action = "!"

View file

@ -7,12 +7,13 @@ description: Release notes and stats on aider writing its own code.
# Release history
{% 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)
Aider writes most of its own code, usually about 70-80% of the new code in each release.
These
[statistics are based on the git commit history](/docs/faq.html#how-are-the-aider-wrote-xx-of-code-stats-computed)
of the aider repo.
{% include blame.md %}
## Release notes
<!--[[[cog
@ -25,8 +26,213 @@ cog.out(text)
### main branch
- Add support for `gemini-2.5-flash-preview-04-17` models.
- Improved "diff" format for Gemini 2.5 Flash by accepting filenames provided on the same line as the opening fence.
- Add new `udiff-simple` edit format, for Gemini 2.5 Pro.
- Update default weak/editor models for Gemini 2.5 Pro models to use `gemini-2.5-flash-preview-04-17`.
- Aider wrote 69% of the code in this release.
### Aider v0.82.2
- Fix editing shell files with diff-fenced, by zjy1412.
- Improve robustness of patch application by allowing multiple update/delete actions for the same file within a single response.
- Update prompts to instruct LLMs to consolidate all edits for a given file into a single block within the patch.
### Aider v0.82.1
- Added support for `o3` and `o4-mini` including provider-specific versions for OpenAI, OpenRouter, and Azure.
- Added support for Azure specific `gpt-4.1` and `gpt-4.1-mini` models.
- Disabled streaming for `o3` models since you need identity verification to stream.
- Fixed handling of file paths in unified diffs, especially those generated by git.
### Aider v0.82.0
- Support for GPT 4.1, mini and nano.
- Added new `patch` edit format for OpenAI's GPT-4.1 model.
- Improved support for using architect mode with Gemini 2.5 Pro.
- Added new `editor-diff`, `editor-whole`, and `editor-diff-fenced` edit formats.
- Bugfix for automatically selecting the best edit format to use in architect mode.
- Added support for `grok-3-fast-beta` and `grok-3-mini-fast-beta` models.
- Aider wrote 92% of the code in this release.
### Aider v0.81.3
- Commit messages generated by aider are no longer forced to be entirely lowercase, by Peter Hadlaw.
- Updated default settings for Grok models.
### Aider v0.81.2
- Add support for `xai/grok-3-beta`, `xai/grok-3-mini-beta`, `openrouter/x-ai/grok-3-beta`, `openrouter/x-ai/grok-3-mini-beta`, and `openrouter/openrouter/optimus-alpha` models.
- Add alias "grok3" for `xai/grok-3-beta`.
- Add alias "optimus" for `openrouter/openrouter/optimus-alpha`.
- Fix URL extraction from error messages.
- Allow adding files by full path even if a file with the same basename is already in the chat.
- Fix quoting of values containing '#' in the sample `aider.conf.yml`.
- Add support for Fireworks AI model 'deepseek-v3-0324', by Felix Lisczyk.
- Commit messages generated by aider are now lowercase, by Anton Ödman.
### Aider v0.81.1
- Added support for the `gemini/gemini-2.5-pro-preview-03-25` model.
- Updated the `gemini` alias to point to `gemini/gemini-2.5-pro-preview-03-25`.
- Added the `gemini-exp` alias for `gemini/gemini-2.5-pro-exp-03-25`.
### Aider v0.81.0
- Added support for the `openrouter/openrouter/quasar-alpha` model.
- Run with `aider --model quasar`
- Offer OpenRouter OAuth authentication if an OpenRouter model is specified but the API key is missing.
- Prevent retrying API calls when the provider reports insufficient credits.
- Improve URL detection to exclude trailing double quotes.
- Aider wrote 86% of the code in this release.
### Aider v0.80.4
- Bumped deps to pickup litellm change to properly display the root cause of OpenRouter "choices" errors.
### Aider v0.80.3
- Improve error message for OpenRouter API connection issues to mention potential rate limiting or upstream provider issues.
- Configure weak models (`gemini/gemini-2.0-flash` and `openrouter/google/gemini-2.0-flash-exp:free`) for Gemini 2.5 Pro models.
- Add model metadata for `openrouter/google/gemini-2.0-flash-exp:free`.
### Aider v0.80.2
- Bumped deps.
### Aider v0.80.1
- Updated deps for yanked fsspec and aiohttp packages #3699
- Removed redundant dependency check during OpenRouter OAuth flow, by Claudia Pellegrino.
### Aider v0.80.0
- OpenRouter OAuth integration:
- Offer to OAuth against OpenRouter if no model and keys are provided.
- Select OpenRouter default model based on free/paid tier status if `OPENROUTER_API_KEY` is set and no model is specified.
- Prioritize `gemini/gemini-2.5-pro-exp-03-25` if `GEMINI_API_KEY` is set, and `vertex_ai/gemini-2.5-pro-exp-03-25` if `VERTEXAI_PROJECT` is set, when no model is specified.
- Validate user-configured color settings on startup and warn/disable invalid ones.
- Warn at startup if `--stream` and `--cache-prompts` are used together, as cost estimates may be inaccurate.
- Boost repomap ranking for files whose path components match identifiers mentioned in the chat.
- Change web scraping timeout from an error to a warning, allowing scraping to continue with potentially incomplete content.
- Left-align markdown headings in the terminal output, by Peter Schilling.
- Update edit format to the new model's default when switching models with `/model`, if the user was using the old model's default format.
- Add `Ctrl-X Ctrl-E` keybinding to edit the current input buffer in an external editor, by Matteo Landi.
- Fix linting errors for filepaths containing shell metacharacters, by Mir Adnan ALI.
- Add the `openrouter/deepseek-chat-v3-0324:free` model.
- Add repomap support for the Scala language, by Vasil Markoukin.
- Fixed bug in `/run` that was preventing auto-testing.
- Fix bug preventing `UnboundLocalError` during git tree traversal.
- Handle `GitCommandNotFound` error if git is not installed or not in PATH.
- Handle `FileNotFoundError` if the current working directory is deleted while aider is running.
- Fix completion menu current item color styling, by Andrey Ivanov.
- Aider wrote 87% of the code in this release.
### Aider v0.79.2
- Added 'gemini' alias for gemini-2.5-pro model.
- Updated Gemini 2.5 Pro max output tokens to 64k.
- Added support for Lisp-style semicolon comments in file watcher, by Matteo Landi.
- Added OpenRouter API error detection and retries.
- Added openrouter/deepseek-chat-v3-0324 model.
- Aider wrote 93% of the code in this release.
### Aider v0.79.1
- Improved model listing to include all models in fuzzy matching, including those provided by aider (not litellm).
### Aider v0.79.0
- Added support for Gemini 2.5 Pro models.
- Added support for DeepSeek V3 0324 model.
- Added a new `/context` command that automatically identifies which files need to be edited for a given request.
- Added `/edit` as an alias for the `/editor` command.
- Added "overeager" mode for Claude 3.7 Sonnet models to try and keep it working within the requested scope.
- Aider wrote 65% of the code in this release.
### Aider v0.78.0
- Added support for thinking tokens for OpenRouter Sonnet 3.7.
- Added commands to switch between model types: `/editor-model` for Editor Model, and `/weak-model` for Weak Model, by csala.
- Added model setting validation to ignore `--reasoning-effort` and `--thinking-tokens` if the model doesn't support them.
- Added `--check-model-accepts-settings` flag (default: true) to force unsupported model settings.
- Annotated which models support reasoning_effort and thinking_tokens settings in the model settings data.
- Improved code block rendering in markdown output with better padding using NoInsetMarkdown.
- Added `--git-commit-verify` flag (default: False) to control whether git commit hooks are bypassed.
- Fixed autocompletion for `/ask`, `/code`, and `/architect` commands, by shladnik.
- Added vi-like behavior when pressing enter in multiline-mode while in vi normal/navigation-mode, by Marco Mayer.
- Added AWS_PROFILE support for Bedrock models, allowing use of AWS profiles instead of explicit credentials, by lentil32.
- Enhanced `--aiderignore` argument to resolve both absolute and relative paths, by mopemope.
- Improved platform information handling to gracefully handle retrieval errors.
- Aider wrote 92% of the code in this release.
### Aider v0.77.1
- Bumped dependencies to pickup litellm fix for Ollama.
- Added support for `openrouter/google/gemma-3-27b-it` model.
- Updated exclude patterns for help documentation.
### Aider v0.77.0
- Big upgrade in [programming languages supported](https://aider.chat/docs/languages.html) by adopting [tree-sitter-language-pack](https://github.com/Goldziher/tree-sitter-language-pack/).
- 130 new languages with linter support.
- 20 new languages with repo-map support.
- Added `/think-tokens` command to set thinking token budget with support for human-readable formats (8k, 10.5k, 0.5M).
- Added `/reasoning-effort` command to control model reasoning level.
- The `/think-tokens` and `/reasoning-effort` commands display current settings when called without arguments.
- Display of thinking token budget and reasoning effort in model information.
- Changed `--thinking-tokens` argument to accept string values with human-readable formats.
- Added `--auto-accept-architect` flag (default: true) to automatically accept changes from architect coder format without confirmation.
- Added support for `cohere_chat/command-a-03-2025` and `gemini/gemma-3-27b-it`
- The bare `/drop` command now preserves original read-only files provided via args.read.
- Fixed a bug where default model would be set by deprecated `--shortcut` switches even when already specified in the command line.
- Improved AutoCompleter to require 3 characters for autocompletion to reduce noise.
- Aider wrote 72% of the code in this release.
### Aider v0.76.2
- Fixed handling of JSONDecodeError when loading model cache file.
- Fixed handling of GitCommandError when retrieving git user configuration.
- Aider wrote 75% of the code in this release.
### Aider v0.76.1
- Added ignore_permission_denied option to file watcher to prevent errors when accessing restricted files, by Yutaka Matsubara.
- Aider wrote 0% of the code in this release.
### Aider v0.76.0
- Improved support for thinking/reasoningmodels:
- Added `--thinking-tokens` CLI option to control token budget for models that support thinking.
- Display thinking/reasoning content from LLMs which return it.
- Enhanced handling of reasoning tags to better clean up model responses.
- Added deprecation warning for `remove_reasoning` setting, now replaced by `reasoning_tag`.
- Aider will notify you when it's completed the last request and needs your input:
- Added [notifications when LLM responses are ready](https://aider.chat/docs/usage/notifications.html) with `--notifications` flag.
- Specify desktop notification command with `--notifications-command`.
- Added support for QWQ 32B.
- Switch to `tree-sitter-language-pack` for tree sitter support.
- Improved error handling for EOF (Ctrl+D) in user input prompts.
- Added helper function to ensure hex color values have a # prefix.
- Fixed handling of Git errors when reading staged files.
- Improved SSL verification control for model information requests.
- Improved empty LLM response handling with clearer warning messages.
- Fixed Git identity retrieval to respect global configuration, by Akira Komamura.
- Offer to install dependencies for Bedrock and Vertex AI models.
- Deprecated model shortcut args (like --4o, --opus) in favor of the --model flag.
- Aider wrote 85% of the code in this release.
### Aider v0.75.3
- Support for V3 free on OpenRouter: `--model openrouter/deepseek/deepseek-chat:free`.
### Aider v0.75.2
- Added support for Claude 3.7 Sonnet models on OpenRouter, Bedrock and Vertex AI.
- Aider wrote 47% of the code in this release.
- Updated default model to Claude 3.7 Sonnet on OpenRouter.
- Added support for GPT-4.5-preview model.
- Added support for Claude 3.7 Sonnet:beta on OpenRouter.
- Fixed weak_model_name patterns to match main model name patterns for some models.
### Aider v0.75.1

View file

@ -51,4 +51,19 @@ callouts:
note:
title: Note
color: yellow
# Custom CSS for our table of contents
kramdown:
syntax_highlighter_opts:
css_class: highlight
sass:
style: compressed
# Additional CSS
compress_html:
clippings: all
comments: all
endings: all
startings: []

View file

@ -3723,7 +3723,7 @@
Titusz Pan: 9
start_tag: v0.71.0
total_lines: 283
- aider_percentage: 69.44
- aider_percentage: 37.47
aider_total: 284
end_date: '2025-01-31'
end_tag: v0.73.0
@ -3746,6 +3746,10 @@
aider/models.py:
Paul Gauthier: 8
Paul Gauthier (aider): 33
aider/resources/model-settings.yml:
Paul Gauthier: 334
kennyfrc: 11
xqyz: 4
aider/sendchat.py:
Mir Adnan ALI: 28
Paul Gauthier: 11
@ -3770,12 +3774,13 @@
Paul Gauthier (aider): 77
grand_total:
Mir Adnan ALI: 28
Paul Gauthier: 96
Paul Gauthier: 430
Paul Gauthier (aider): 284
xqyz: 1
kennyfrc: 11
xqyz: 5
start_tag: v0.72.0
total_lines: 409
- aider_percentage: 77.14
total_lines: 758
- aider_percentage: 76.07
aider_total: 604
end_date: '2025-02-06'
end_tag: v0.74.0
@ -3813,6 +3818,8 @@
Paul Gauthier: 1
Paul Gauthier (aider): 2
"Viktor Sz\xE9pe": 3
aider/resources/model-settings.yml:
Paul Gauthier: 11
aider/watch.py:
Paul Gauthier (aider): 45
benchmark/docker.sh:
@ -3839,12 +3846,12 @@
Paul Gauthier: 4
Paul Gauthier (aider): 42
grand_total:
Paul Gauthier: 176
Paul Gauthier: 187
Paul Gauthier (aider): 604
"Viktor Sz\xE9pe": 3
start_tag: v0.73.0
total_lines: 783
- aider_percentage: 46.31
total_lines: 794
- aider_percentage: 44.78
aider_total: 163
end_date: '2025-02-24'
end_tag: v0.75.0
@ -3880,6 +3887,8 @@
aider/repomap.py:
Paul Gauthier: 43
Paul Gauthier (aider): 11
aider/resources/model-settings.yml:
Paul Gauthier: 12
aider/special.py:
Lucas Shadler: 1
aider/website/docs/leaderboards/index.md:
@ -3909,8 +3918,585 @@
Antti Kaihola: 1
FeepingCreature (aider): 6
Lucas Shadler: 1
Paul Gauthier: 113
Paul Gauthier: 125
Paul Gauthier (aider): 157
Warren Krewenki: 74
start_tag: v0.74.0
total_lines: 352
total_lines: 364
- aider_percentage: 84.75
aider_total: 1589
end_date: '2025-03-10'
end_tag: v0.76.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/args.py:
Paul Gauthier: 2
Paul Gauthier (aider): 25
aider/args_formatter.py:
Paul Gauthier: 4
Paul Gauthier (aider): 3
aider/coders/base_coder.py:
Paul Gauthier: 54
Paul Gauthier (aider): 29
aider/deprecated.py:
Paul Gauthier (aider): 107
aider/io.py:
Paul Gauthier: 7
Paul Gauthier (aider): 127
aider/main.py:
Akira Komamura: 2
Mattias: 1
Paul Gauthier: 4
Paul Gauthier (aider): 16
aider/models.py:
Paul Gauthier: 6
Paul Gauthier (aider): 68
aider/queries/tree-sitter-language-pack/csharp-tags.scm:
Paul Gauthier: 14
Paul Gauthier (aider): 12
aider/reasoning_tags.py:
Paul Gauthier: 14
Paul Gauthier (aider): 68
aider/repo.py:
Akira Komamura: 1
Paul Gauthier (aider): 4
aider/repomap.py:
Paul Gauthier: 9
aider/resources/model-settings.yml:
Paul Gauthier: 61
Paul Gauthier (aider): 32
gmoz22: 4
aider/website/_includes/leaderboard.js:
Paul Gauthier (aider): 48
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 2
benchmark/benchmark.py:
Paul Gauthier: 1
benchmark/problem_stats.py:
Paul Gauthier (aider): 2
docker/Dockerfile:
Paul Gauthier: 1
scripts/blame.py:
Paul Gauthier: 1
scripts/pip-compile.sh:
Claudia Pellegrino: 10
Paul Gauthier: 6
Paul Gauthier (aider): 11
scripts/update-history.py:
Paul Gauthier: 1
scripts/versionbump.py:
Paul Gauthier: 4
Paul Gauthier (aider): 64
tests/basic/test_deprecated.py:
Paul Gauthier: 10
Paul Gauthier (aider): 130
tests/basic/test_io.py:
Paul Gauthier (aider): 54
tests/basic/test_main.py:
Paul Gauthier: 1
Paul Gauthier (aider): 93
tests/basic/test_model_info_manager.py:
Paul Gauthier (aider): 72
tests/basic/test_models.py:
Paul Gauthier: 27
Paul Gauthier (aider): 34
tests/basic/test_reasoning.py:
Paul Gauthier: 36
Paul Gauthier (aider): 525
tests/basic/test_repomap.py:
Paul Gauthier: 2
tests/basic/test_ssl_verification.py:
Paul Gauthier (aider): 65
grand_total:
Akira Komamura: 3
Claudia Pellegrino: 10
Mattias: 1
Paul Gauthier: 268
Paul Gauthier (aider): 1589
gmoz22: 4
start_tag: v0.75.0
total_lines: 1875
- aider_percentage: 71.93
aider_total: 1399
end_date: '2025-03-13'
end_tag: v0.77.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/args.py:
Paul Gauthier (aider): 5
aider/coders/architect_coder.py:
Paul Gauthier (aider): 2
aider/coders/base_coder.py:
Paul Gauthier (aider): 14
aider/commands.py:
Paul Gauthier: 4
Paul Gauthier (aider): 71
aider/deprecated.py:
Paul Gauthier: 2
aider/io.py:
Paul Gauthier (aider): 5
aider/main.py:
Paul Gauthier (aider): 12
aider/models.py:
Paul Gauthier (aider): 83
aider/queries/tree-sitter-language-pack/arduino-tags.scm:
Paul Gauthier: 3
Paul Gauthier (aider): 2
aider/queries/tree-sitter-language-pack/c-tags.scm:
Paul Gauthier: 4
Paul Gauthier (aider): 5
aider/queries/tree-sitter-language-pack/chatito-tags.scm:
Paul Gauthier: 11
Paul Gauthier (aider): 5
aider/queries/tree-sitter-language-pack/commonlisp-tags.scm:
Paul Gauthier: 116
Paul Gauthier (aider): 6
aider/queries/tree-sitter-language-pack/cpp-tags.scm:
Paul Gauthier: 7
Paul Gauthier (aider): 8
aider/queries/tree-sitter-language-pack/d-tags.scm:
Paul Gauthier: 9
Paul Gauthier (aider): 17
aider/queries/tree-sitter-language-pack/dart-tags.scm:
Paul Gauthier: 42
Paul Gauthier (aider): 19
aider/queries/tree-sitter-language-pack/elisp-tags.scm:
Paul Gauthier: 1
Paul Gauthier (aider): 2
aider/queries/tree-sitter-language-pack/elixir-tags.scm:
Paul Gauthier: 10
Paul Gauthier (aider): 8
aider/queries/tree-sitter-language-pack/elm-tags.scm:
Paul Gauthier: 8
Paul Gauthier (aider): 11
aider/queries/tree-sitter-language-pack/gleam-tags.scm:
Paul Gauthier: 26
Paul Gauthier (aider): 15
aider/queries/tree-sitter-language-pack/go-tags.scm:
Paul Gauthier: 14
Paul Gauthier (aider): 14
aider/queries/tree-sitter-language-pack/java-tags.scm:
Paul Gauthier: 10
Paul Gauthier (aider): 7
aider/queries/tree-sitter-language-pack/lua-tags.scm:
Paul Gauthier: 25
Paul Gauthier (aider): 9
aider/queries/tree-sitter-language-pack/pony-tags.scm:
Paul Gauthier: 20
Paul Gauthier (aider): 19
aider/queries/tree-sitter-language-pack/properties-tags.scm:
Paul Gauthier: 3
Paul Gauthier (aider): 2
aider/queries/tree-sitter-language-pack/python-tags.scm:
Paul Gauthier: 9
Paul Gauthier (aider): 5
aider/queries/tree-sitter-language-pack/r-tags.scm:
Paul Gauthier: 17
Paul Gauthier (aider): 4
aider/queries/tree-sitter-language-pack/racket-tags.scm:
Paul Gauthier: 10
Paul Gauthier (aider): 2
aider/queries/tree-sitter-language-pack/ruby-tags.scm:
Paul Gauthier: 23
Paul Gauthier (aider): 12
aider/queries/tree-sitter-language-pack/rust-tags.scm:
Paul Gauthier: 41
Paul Gauthier (aider): 14
aider/queries/tree-sitter-language-pack/solidity-tags.scm:
Paul Gauthier: 30
Paul Gauthier (aider): 13
aider/queries/tree-sitter-language-pack/swift-tags.scm:
Paul Gauthier: 39
Paul Gauthier (aider): 12
aider/queries/tree-sitter-language-pack/udev-tags.scm:
Paul Gauthier: 15
Paul Gauthier (aider): 5
aider/resources/model-settings.yml:
Paul Gauthier: 9
aider/watch.py:
Yutaka Matsubara: 4
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 3
Paul Gauthier (aider): 8
scripts/redact-cast.py:
Paul Gauthier: 27
Paul Gauthier (aider): 98
scripts/tsl_pack_langs.py:
Paul Gauthier (aider): 145
scripts/versionbump.py:
Paul Gauthier (aider): 1
tests/basic/test_coder.py:
Paul Gauthier (aider): 104
tests/basic/test_commands.py:
Paul Gauthier: 2
Paul Gauthier (aider): 190
tests/basic/test_models.py:
Paul Gauthier (aider): 44
tests/basic/test_repomap.py:
Paul Gauthier: 1
Paul Gauthier (aider): 125
tests/fixtures/languages/arduino/test.ino:
Paul Gauthier (aider): 21
tests/fixtures/languages/c/test.c:
Paul Gauthier (aider): 12
tests/fixtures/languages/chatito/test.chatito:
Paul Gauthier (aider): 20
tests/fixtures/languages/commonlisp/test.lisp:
Paul Gauthier (aider): 17
tests/fixtures/languages/d/test.d:
Paul Gauthier (aider): 26
tests/fixtures/languages/dart/test.dart:
Paul Gauthier (aider): 21
tests/fixtures/languages/elm/test.elm:
Paul Gauthier (aider): 16
tests/fixtures/languages/gleam/test.gleam:
Paul Gauthier (aider): 10
tests/fixtures/languages/lua/test.lua:
Paul Gauthier (aider): 25
tests/fixtures/languages/pony/test.pony:
Paul Gauthier (aider): 8
tests/fixtures/languages/properties/test.properties:
Paul Gauthier (aider): 14
tests/fixtures/languages/r/test.r:
Paul Gauthier (aider): 17
tests/fixtures/languages/racket/test.rkt:
Paul Gauthier (aider): 8
tests/fixtures/languages/solidity/test.sol:
Paul Gauthier (aider): 21
tests/fixtures/languages/swift/test.swift:
Paul Gauthier (aider): 18
tests/fixtures/languages/udev/test.rules:
Paul Gauthier (aider): 22
grand_total:
Paul Gauthier: 542
Paul Gauthier (aider): 1399
Yutaka Matsubara: 4
start_tag: v0.76.0
total_lines: 1945
- aider_percentage: 91.82
aider_total: 2682
end_date: '2025-03-21'
end_tag: v0.78.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/args.py:
Paul Gauthier (aider): 24
Yutaka Matsubara: 2
aider/coders/base_coder.py:
Paul Gauthier: 1
Paul Gauthier (aider): 6
aider/commands.py:
Carles Sala (aider): 30
Paul Gauthier (aider): 10
aider/help_pats.py:
Paul Gauthier: 6
aider/io.py:
Marco Mayer: 2
Paul Gauthier (aider): 17
aider/main.py:
Paul Gauthier: 5
Paul Gauthier (aider): 29
aider/mdstream.py:
Paul Gauthier: 1
Paul Gauthier (aider): 22
aider/models.py:
Paul Gauthier (aider): 41
lentil32 (aider): 15
aider/repo.py:
Paul Gauthier (aider): 5
aider/resources/model-settings.yml:
Paul Gauthier: 3
Paul Gauthier (aider): 22
aider/website/_includes/head_custom.html:
Paul Gauthier: 3
Paul Gauthier (aider): 53
aider/website/_includes/recording.js:
Paul Gauthier: 4
Paul Gauthier (aider): 424
aider/website/assets/asciinema/asciinema-player.min.js:
Paul Gauthier: 1
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 1
aider/website/index.html:
Paul Gauthier: 173
Paul Gauthier (aider): 371
scripts/badges.py:
Paul Gauthier: 1
Paul Gauthier (aider): 496
scripts/blame.py:
Paul Gauthier: 2
scripts/jekyll_run.sh:
Paul Gauthier: 1
Paul Gauthier (aider): 5
scripts/logo_svg.py:
Paul Gauthier: 5
Paul Gauthier (aider): 169
scripts/recording_audio.py:
Paul Gauthier (aider): 338
scripts/redact-cast.py:
Paul Gauthier: 22
Paul Gauthier (aider): 37
scripts/tmux_record.sh:
Paul Gauthier: 1
Paul Gauthier (aider): 17
scripts/update-docs.sh:
Paul Gauthier: 1
scripts/update-history.py:
Paul Gauthier: 1
Paul Gauthier (aider): 52
tests/basic/test_aws_credentials.py:
lentil32 (aider): 169
tests/basic/test_commands.py:
Carles Sala (aider): 40
tests/basic/test_main.py:
Paul Gauthier: 2
Paul Gauthier (aider): 193
tests/basic/test_repo.py:
Paul Gauthier (aider): 48
tests/help/test_help.py:
Paul Gauthier (aider): 49
grand_total:
Carles Sala (aider): 70
Marco Mayer: 2
Paul Gauthier: 235
Paul Gauthier (aider): 2428
Yutaka Matsubara: 2
lentil32 (aider): 184
start_tag: v0.77.0
total_lines: 2921
- aider_percentage: 65.38
aider_total: 221
end_date: '2025-03-25'
end_tag: v0.79.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/coders/__init__.py:
Paul Gauthier: 2
aider/coders/base_coder.py:
Paul Gauthier: 15
Paul Gauthier (aider): 5
aider/coders/context_coder.py:
Paul Gauthier: 45
Paul Gauthier (aider): 8
aider/commands.py:
Paul Gauthier: 1
Paul Gauthier (aider): 20
aider/io.py:
Paul Gauthier: 11
Paul Gauthier (aider): 2
aider/main.py:
Paul Gauthier (aider): 4
aider/models.py:
Paul Gauthier: 3
Paul Gauthier (aider): 1
aider/repomap.py:
Paul Gauthier: 17
aider/resources/model-settings.yml:
Paul Gauthier: 13
Paul Gauthier (aider): 10
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 1
aider/website/index.html:
Paul Gauthier: 3
Paul Gauthier (aider): 16
scripts/badges.py:
Paul Gauthier (aider): 2
scripts/blame.py:
Paul Gauthier (aider): 16
scripts/dl_icons.py:
Paul Gauthier (aider): 60
scripts/tmux_record.sh:
Paul Gauthier: 1
tests/basic/test_coder.py:
Paul Gauthier: 4
Paul Gauthier (aider): 77
grand_total:
Paul Gauthier: 117
Paul Gauthier (aider): 221
start_tag: v0.78.0
total_lines: 338
- aider_percentage: 86.86
aider_total: 1837
end_date: '2025-03-31'
end_tag: v0.80.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/coders/base_coder.py:
Paul Gauthier: 2
aider/commands.py:
Paul Gauthier: 4
Paul Gauthier (aider): 20
aider/exceptions.py:
Paul Gauthier: 1
Paul Gauthier (aider): 3
aider/io.py:
Andrey Ivanov: 2
Matteo Landi (aider): 11
Paul Gauthier (aider): 38
aider/linter.py:
Mir Adnan ALI: 2
aider/main.py:
Paul Gauthier: 1
Paul Gauthier (aider): 21
aider/mdstream.py:
Peter Schilling (aider) (aider): 25
aider/models.py:
Paul Gauthier: 12
Paul Gauthier (aider): 9
aider/onboarding.py:
Paul Gauthier: 44
Paul Gauthier (aider): 389
aider/queries/tree-sitter-languages/scala-tags.scm:
Vasil Markoukin: 65
aider/repo.py:
Paul Gauthier: 1
Paul Gauthier (aider): 7
aider/repomap.py:
Paul Gauthier (aider): 19
aider/resources/model-settings.yml:
Paul Gauthier (aider): 13
aider/scrape.py:
Paul Gauthier: 1
Paul Gauthier (aider): 1
aider/utils.py:
Paul Gauthier (aider): 5
aider/watch.py:
Matteo Landi (aider): 2
aider/website/_includes/leaderboard.js:
Paul Gauthier: 1
Paul Gauthier (aider): 2
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 1
aider/website/index.html:
Paul Gauthier: 51
Paul Gauthier (aider): 175
scripts/30k-image.py:
Paul Gauthier: 8
Paul Gauthier (aider): 227
scripts/homepage.py:
Paul Gauthier (aider): 122
tests/basic/test_commands.py:
Paul Gauthier: 2
Paul Gauthier (aider): 48
tests/basic/test_exceptions.py:
Paul Gauthier (aider): 17
tests/basic/test_io.py:
Paul Gauthier (aider): 28
tests/basic/test_main.py:
Paul Gauthier: 15
Paul Gauthier (aider): 199
tests/basic/test_onboarding.py:
Paul Gauthier (aider): 439
tests/basic/test_repomap.py:
Vasil Markoukin: 3
tests/basic/test_ssl_verification.py:
Paul Gauthier (aider): 8
tests/basic/test_watch.py:
Matteo Landi (aider): 9
tests/fixtures/languages/scala/test.scala:
Vasil Markoukin: 61
grand_total:
Andrey Ivanov: 2
Matteo Landi (aider): 22
Mir Adnan ALI: 2
Paul Gauthier: 145
Paul Gauthier (aider): 1790
Peter Schilling (aider) (aider): 25
Vasil Markoukin: 129
start_tag: v0.79.0
total_lines: 2115
- aider_percentage: 85.55
aider_total: 225
end_date: '2025-04-04'
end_tag: v0.81.0
file_counts:
.github/workflows/check_pypi_version.yml:
Paul Gauthier: 11
Paul Gauthier (aider): 75
.github/workflows/windows_check_pypi_version.yml:
Paul Gauthier: 4
Paul Gauthier (aider): 86
aider/__init__.py:
Paul Gauthier: 1
aider/coders/base_coder.py:
Paul Gauthier (aider): 4
aider/exceptions.py:
Paul Gauthier: 6
Paul Gauthier (aider): 12
aider/main.py:
Paul Gauthier (aider): 40
aider/models.py:
Paul Gauthier (aider): 2
aider/resources/model-settings.yml:
Paul Gauthier: 9
Paul Gauthier (aider): 1
aider/website/_includes/leaderboard.js:
Paul Gauthier (aider): 5
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 1
aider/website/index.html:
Paul Gauthier: 3
tests/basic/test_exceptions.py:
Paul Gauthier: 3
grand_total:
Paul Gauthier: 38
Paul Gauthier (aider): 225
start_tag: v0.80.0
total_lines: 263
- aider_percentage: 91.85
aider_total: 1567
end_date: '2025-04-14'
end_tag: v0.82.0
file_counts:
aider/__init__.py:
Paul Gauthier: 1
aider/args_formatter.py:
Paul Gauthier (aider): 4
aider/coders/__init__.py:
Paul Gauthier (aider): 4
aider/coders/base_coder.py:
Paul Gauthier: 4
Paul Gauthier (aider): 5
aider/coders/editor_diff_fenced_coder.py:
Paul Gauthier (aider): 9
aider/coders/patch_coder.py:
Paul Gauthier (aider): 679
aider/coders/search_replace.py:
Paul Gauthier (aider): 1
aider/main.py:
Paul Gauthier (aider): 1
aider/models.py:
Paul Gauthier: 1
Paul Gauthier (aider): 25
aider/resources/model-settings.yml:
Felix Lisczyk: 13
Paul Gauthier: 37
Paul Gauthier (aider): 68
aider/website/_includes/leaderboard.js:
Paul Gauthier: 38
Paul Gauthier (aider): 6
aider/website/_includes/leaderboard_table.js:
Paul Gauthier (aider): 518
aider/website/docs/leaderboards/index.md:
Paul Gauthier: 15
Paul Gauthier (aider): 209
aider/website/index.html:
Paul Gauthier: 28
scripts/homepage.py:
Paul Gauthier (aider): 2
scripts/versionbump.py:
Paul Gauthier (aider): 11
tests/basic/test_coder.py:
Paul Gauthier: 2
Paul Gauthier (aider): 25
grand_total:
Felix Lisczyk: 13
Paul Gauthier: 126
Paul Gauthier (aider): 1567
start_tag: v0.81.0
total_lines: 1706

View file

@ -256,4 +256,4 @@
date: 2024-12-22
versions: 0.69.2.dev
seconds_per_case: 12.2
total_cost: 0.0000
total_cost: 0.0000

View file

@ -1,6 +1,6 @@
- dirname: 2025-02-25-20-23-07--gemini-pro
test_cases: 225
model: gemini/gemini-2.0-pro-exp-02-05
model: Gemini 2.0 Pro exp-02-05
edit_format: whole
commit_hash: 2fccd47
pass_rate_1: 20.4
@ -338,7 +338,7 @@
- dirname: 2024-12-25-13-31-51--deepseekv3preview-diff2
test_cases: 225
model: DeepSeek Chat V3
model: DeepSeek Chat V3 (prev)
edit_format: diff
commit_hash: 0a23c4a-dirty
pass_rate_1: 22.7
@ -643,7 +643,7 @@
exhausted_context_windows: 0
test_timeouts: 1
total_tests: 225
command: "aider --model anthropic/claude-3-7-sonnet-20250219 # plus yml config"
command: "aider --model anthropic/claude-3-7-sonnet-20250219 --thinking-tokens 32k"
date: 2025-02-24
versions: 0.75.1.dev
seconds_per_case: 105.2
@ -673,4 +673,554 @@
date: 2025-02-27
versions: 0.75.2.dev
seconds_per_case: 113.5
total_cost: 183.1802
total_cost: 183.1802
- dirname: 2025-03-06-17-40-24--qwq32b-diff-temp-topp-ex-sys-remind-user-for-real
test_cases: 225
model: QwQ-32B
edit_format: diff
commit_hash: 51d118f-dirty
pass_rate_1: 8.0
pass_rate_2: 20.9
pass_num_1: 18
pass_num_2: 47
percent_cases_well_formed: 67.6
error_outputs: 145
num_malformed_responses: 143
num_with_malformed_responses: 73
user_asks: 17
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 4
total_tests: 225
command: aider --model fireworks_ai/accounts/fireworks/models/qwq-32b
date: 2025-03-06
versions: 0.75.3.dev
seconds_per_case: 228.6
total_cost: 0.0000
- dirname: 2025-03-07-15-11-27--qwq32b-arch-temp-topp-again
test_cases: 225
model: QwQ-32B + Qwen 2.5 Coder Instruct
edit_format: architect
commit_hash: 52162a5
editor_model: fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct
editor_edit_format: editor-diff
pass_rate_1: 9.8
pass_rate_2: 26.2
pass_num_1: 22
pass_num_2: 59
percent_cases_well_formed: 100.0
error_outputs: 122
num_malformed_responses: 0
num_with_malformed_responses: 0
user_asks: 489
lazy_comments: 8
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 2
total_tests: 225
command: aider --model fireworks_ai/accounts/fireworks/models/qwq-32b --architect
date: 2025-03-07
versions: 0.75.3.dev
seconds_per_case: 137.4
total_cost: 0
- dirname: 2025-03-14-23-40-00--cmda-quality-whole2
test_cases: 225
model: command-a-03-2025-quality
edit_format: whole
commit_hash: a1aa63f
pass_rate_1: 2.2
pass_rate_2: 12.0
pass_num_1: 5
pass_num_2: 27
percent_cases_well_formed: 99.6
error_outputs: 2
num_malformed_responses: 1
num_with_malformed_responses: 1
user_asks: 215
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 4
total_tests: 225
command: OPENAI_API_BASE=https://api.cohere.ai/compatibility/v1 aider --model openai/command-a-03-2025-quality
date: 2025-03-14
versions: 0.77.1.dev
seconds_per_case: 85.1
total_cost: 0.0000
- dirname: 2025-03-15-01-21-24--gemma3-27b-or
test_cases: 225
model: gemma-3-27b-it
edit_format: whole
commit_hash: fd21f51-dirty
pass_rate_1: 1.8
pass_rate_2: 4.9
pass_num_1: 4
pass_num_2: 11
percent_cases_well_formed: 100.0
error_outputs: 3
num_malformed_responses: 0
num_with_malformed_responses: 0
user_asks: 181
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 3
total_tests: 225
command: aider --model openrouter/google/gemma-3-27b-it
date: 2025-03-15
versions: 0.77.1.dev
seconds_per_case: 79.7
total_cost: 0.0000
- dirname: 2025-03-24-15-41-33--deepseek-v3-0324-polyglot-diff
test_cases: 225
model: DeepSeek V3 (0324)
edit_format: diff
commit_hash: 502b863
pass_rate_1: 28.0
pass_rate_2: 55.1
pass_num_1: 63
pass_num_2: 124
percent_cases_well_formed: 99.6
error_outputs: 32
num_malformed_responses: 1
num_with_malformed_responses: 1
user_asks: 96
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 2
test_timeouts: 4
total_tests: 225
command: aider --model deepseek/deepseek-chat
date: 2025-03-24
versions: 0.78.1.dev
seconds_per_case: 290.0
total_cost: 1.1164
- dirname: 2025-04-12-04-55-50--gemini-25-pro-diff-fenced
test_cases: 225
model: Gemini 2.5 Pro Preview 03-25
edit_format: diff-fenced
commit_hash: 0282574
pass_rate_1: 40.9
pass_rate_2: 72.9
pass_num_1: 92
pass_num_2: 164
percent_cases_well_formed: 92.4
error_outputs: 21
num_malformed_responses: 21
num_with_malformed_responses: 17
user_asks: 69
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 2
total_tests: 225
command: aider --model gemini/gemini-2.5-pro-preview-03-25
date: 2025-04-12
versions: 0.81.3.dev
seconds_per_case: 45.3
total_cost: 6.3174
- dirname: 2025-03-29-05-24-55--chatgpt4o-mar28-diff
test_cases: 225
model: chatgpt-4o-latest (2025-03-29)
edit_format: diff
commit_hash: 0decbad
pass_rate_1: 16.4
pass_rate_2: 45.3
pass_num_1: 37
pass_num_2: 102
percent_cases_well_formed: 64.4
error_outputs: 85
num_malformed_responses: 85
num_with_malformed_responses: 80
user_asks: 174
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 4
total_tests: 225
command: aider --model chatgpt-4o-latest
date: 2025-03-29
versions: 0.79.3.dev
seconds_per_case: 10.3
total_cost: 19.7416
- dirname: 2025-04-04-02-57-25--qalpha-diff-exsys
test_cases: 225
model: Quasar Alpha
edit_format: diff
commit_hash: 8a34a6c-dirty
pass_rate_1: 21.8
pass_rate_2: 54.7
pass_num_1: 49
pass_num_2: 123
percent_cases_well_formed: 98.2
error_outputs: 4
num_malformed_responses: 4
num_with_malformed_responses: 4
user_asks: 187
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 4
total_tests: 225
command: aider --model openrouter/openrouter/quasar-alpha
date: 2025-04-04
versions: 0.80.5.dev
seconds_per_case: 14.8
total_cost: 0.0000
- dirname: 2025-04-06-08-39-52--llama-4-maverick-17b-128e-instruct-polyglot-whole
test_cases: 225
model: Llama 4 Maverick
edit_format: whole
commit_hash: 9445a31
pass_rate_1: 4.4
pass_rate_2: 15.6
pass_num_1: 10
pass_num_2: 35
percent_cases_well_formed: 99.1
error_outputs: 12
num_malformed_responses: 2
num_with_malformed_responses: 2
user_asks: 248
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 4
total_tests: 225
command: aider --model nvidia_nim/meta/llama-4-maverick-17b-128e-instruct
date: 2025-04-06
versions: 0.81.2.dev
seconds_per_case: 20.5
total_cost: 0.0000
- dirname: 2025-04-10-04-21-31--grok3-diff-exuser
test_cases: 225
model: Grok 3 Beta
edit_format: diff
commit_hash: 2dd40fc-dirty
pass_rate_1: 22.2
pass_rate_2: 53.3
pass_num_1: 50
pass_num_2: 120
percent_cases_well_formed: 99.6
error_outputs: 1
num_malformed_responses: 1
num_with_malformed_responses: 1
user_asks: 68
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 2
total_tests: 225
command: aider --model openrouter/x-ai/grok-3-beta
date: 2025-04-10
versions: 0.81.2.dev
seconds_per_case: 15.3
total_cost: 11.0338
- dirname: 2025-04-10-18-47-24--grok3-mini-whole-exuser
test_cases: 225
model: Grok 3 Mini Beta (low)
edit_format: whole
commit_hash: 14ffe77-dirty
pass_rate_1: 11.1
pass_rate_2: 34.7
pass_num_1: 25
pass_num_2: 78
percent_cases_well_formed: 100.0
error_outputs: 3
num_malformed_responses: 0
num_with_malformed_responses: 0
user_asks: 73
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 5
total_tests: 225
command: aider --model openrouter/x-ai/grok-3-mini-beta
date: 2025-04-10
versions: 0.81.2.dev
seconds_per_case: 35.1
total_cost: 0.7856
- dirname: 2025-04-10-23-59-02--xai-grok3-mini-whole-high
test_cases: 225
model: Grok 3 Mini Beta (high)
edit_format: whole
commit_hash: 8ee33da-dirty
pass_rate_1: 17.3
pass_rate_2: 49.3
pass_num_1: 39
pass_num_2: 111
percent_cases_well_formed: 99.6
error_outputs: 1
num_malformed_responses: 1
num_with_malformed_responses: 1
user_asks: 64
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 0
total_tests: 225
command: aider --model xai/grok-3-mini-beta --reasoning-effort high
date: 2025-04-10
versions: 0.81.3.dev
seconds_per_case: 79.1
total_cost: 0.7346
- dirname: 2025-04-10-19-02-44--oalpha-diff-exsys
test_cases: 225
model: Optimus Alpha
edit_format: diff
commit_hash: 532bc45-dirty
pass_rate_1: 21.3
pass_rate_2: 52.9
pass_num_1: 48
pass_num_2: 119
percent_cases_well_formed: 97.3
error_outputs: 7
num_malformed_responses: 6
num_with_malformed_responses: 6
user_asks: 182
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 3
total_tests: 225
command: aider --model openrouter/openrouter/optimus-alpha
date: 2025-04-10
versions: 0.81.2.dev
seconds_per_case: 18.4
total_cost: 0.0000
- dirname: 2025-04-14-21-05-54--gpt41-diff-exuser
test_cases: 225
model: gpt-4.1
edit_format: diff
commit_hash: 7a87db5-dirty
pass_rate_1: 20.0
pass_rate_2: 52.4
pass_num_1: 45
pass_num_2: 118
percent_cases_well_formed: 98.2
error_outputs: 6
num_malformed_responses: 5
num_with_malformed_responses: 4
user_asks: 171
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 5
total_tests: 225
command: aider --model gpt-4.1
date: 2025-04-14
versions: 0.81.4.dev
seconds_per_case: 20.5
total_cost: 9.8556
- dirname: 2025-04-14-21-27-53--gpt41mini-diff
test_cases: 225
model: gpt-4.1-mini
edit_format: diff
commit_hash: ffb743e-dirty
pass_rate_1: 11.1
pass_rate_2: 32.4
pass_num_1: 25
pass_num_2: 73
percent_cases_well_formed: 92.4
error_outputs: 64
num_malformed_responses: 62
num_with_malformed_responses: 17
user_asks: 159
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 2
test_timeouts: 2
total_tests: 225
command: aider --model gpt-4.1-mini
date: 2025-04-14
versions: 0.81.4.dev
seconds_per_case: 19.5
total_cost: 1.9918
- dirname: 2025-04-14-22-46-01--gpt41nano-diff
test_cases: 225
model: gpt-4.1-nano
edit_format: whole
commit_hash: 71d1591-dirty
pass_rate_1: 3.1
pass_rate_2: 8.9
pass_num_1: 7
pass_num_2: 20
percent_cases_well_formed: 94.2
error_outputs: 20
num_malformed_responses: 20
num_with_malformed_responses: 13
user_asks: 316
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 8
total_tests: 225
command: aider --model gpt-4.1-nano
date: 2025-04-14
versions: 0.81.4.dev
seconds_per_case: 12.0
total_cost: 0.4281
- dirname: 2025-04-16-21-20-55--o3-high-diff-temp0-exsys
test_cases: 225
model: o3 (high)
edit_format: diff
commit_hash: 24805ff-dirty
pass_rate_1: 36.9
pass_rate_2: 79.6
pass_num_1: 83
pass_num_2: 179
percent_cases_well_formed: 95.1
error_outputs: 11
num_malformed_responses: 11
num_with_malformed_responses: 11
user_asks: 110
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 2
total_tests: 225
command: aider --model o3
date: 2025-04-16
versions: 0.82.1.dev
seconds_per_case: 113.8
total_cost: 111.0325
- dirname: 2025-04-16-22-01-58--o4-mini-high-diff-exsys
test_cases: 225
model: o4-mini (high)
edit_format: diff
commit_hash: b66901f-dirty
pass_rate_1: 19.6
pass_rate_2: 72.0
pass_num_1: 44
pass_num_2: 162
percent_cases_well_formed: 90.7
error_outputs: 26
num_malformed_responses: 24
num_with_malformed_responses: 21
user_asks: 66
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 1
test_timeouts: 2
total_tests: 225
command: aider --model o4-mini
date: 2025-04-16
versions: 0.82.1.dev
seconds_per_case: 176.5
total_cost: 19.6399
- dirname: 2025-04-17-01-20-35--o3-mini-high-diff-arch
test_cases: 225
model: o3 (high) + gpt-4.1
edit_format: architect
commit_hash: 80909e1-dirty
editor_model: gpt-4.1
editor_edit_format: editor-diff
pass_rate_1: 36.0
pass_rate_2: 82.7
pass_num_1: 81
pass_num_2: 186
percent_cases_well_formed: 100.0
error_outputs: 9
num_malformed_responses: 0
num_with_malformed_responses: 0
user_asks: 166
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 0
total_tests: 225
command: aider --model o3 --architect
date: 2025-04-17
versions: 0.82.2.dev
seconds_per_case: 110.0
total_cost: 69.2921
- dirname: 2025-04-19-14-43-04--o4-mini-patch
test_cases: 225
model: openhands-lm-32b-v0.1
edit_format: whole
commit_hash: c08336f
pass_rate_1: 4.0
pass_rate_2: 10.2
pass_num_1: 9
pass_num_2: 23
percent_cases_well_formed: 95.1
error_outputs: 55
num_malformed_responses: 41
num_with_malformed_responses: 11
user_asks: 166
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
test_timeouts: 11
total_tests: 225
command: aider --model openrouter/all-hands/openhands-lm-32b-v0.1
date: 2025-04-19
versions: 0.82.2.dev
seconds_per_case: 195.6
total_cost: 0.0000
- dirname: 2025-04-20-19-54-31--flash25-diff-no-think
test_cases: 225
model: gemini-2.5-flash-preview-04-17 (default)
edit_format: diff
commit_hash: 7fcce5d-dirty
pass_rate_1: 21.8
pass_rate_2: 47.1
pass_num_1: 49
pass_num_2: 106
percent_cases_well_formed: 85.3
error_outputs: 60
num_malformed_responses: 55
num_with_malformed_responses: 33
user_asks: 82
lazy_comments: 1
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 5
test_timeouts: 4
total_tests: 225
command: aider --model gemini/gemini-2.5-flash-preview-04-17
date: 2025-04-20
versions: 0.82.3.dev
seconds_per_case: 50.1
total_cost: 1.8451

View file

@ -1,25 +1,22 @@
If you already have python 3.8-3.13 installed, you can get started quickly like this:
If you already have python 3.8-3.13 installed, you can get started quickly like this.
First, install aider:
{% include install.md %}
Start working with aider on your codebase:
```bash
python -m pip install aider-install
aider-install
# Change directory into your code base
# Change directory into your codebase
cd /to/your/project
# Work with DeepSeek via DeepSeek's API
aider --model deepseek --api-key deepseek=your-key-goes-here
# DeepSeek
aider --model deepseek --api-key deepseek=<key>
# Work with Claude 3.5 Sonnet via Anthropic's API
aider --model sonnet --api-key anthropic=your-key-goes-here
# Claude 3.7 Sonnet
aider --model sonnet --api-key anthropic=<key>
# Work with GPT-4o via OpenAI's API
aider --model gpt-4o --api-key openai=your-key-goes-here
# Work with Sonnet via OpenRouter's API
aider --model openrouter/anthropic/claude-3.5-sonnet --api-key openrouter=your-key-goes-here
# Work with DeepSeek via OpenRouter's API
aider --model openrouter/deepseek/deepseek-chat --api-key openrouter=your-key-goes-here
# o3-mini
aider --model o3-mini --api-key openai=<key>
```

View file

@ -5,10 +5,66 @@
<meta property="og:image" content="{{ site.url }}/assets/aider.jpg">
<meta property="twitter:image" content="{{ site.url }}/assets/aider-square.jpg">
{% endif %}
<!-- Custom site title styling -->
<style>
@font-face {
font-family: GlassTTYVT220;
src: local("Glass TTY VT220"), local("Glass TTY VT220 Medium"), url(/assets/Glass_TTY_VT220.ttf) format("truetype");
}
.site-title {
font-size: 1.8rem;
font-weight: 700;
font-family: 'GlassTTYVT220', monospace;
color: #14b014; /* terminal green color */
text-decoration: none;
letter-spacing: 0.5px;
}
/* For SVG logo inside site-title */
.site-title img {
height: 1.8rem;
vertical-align: middle;
}
/* Sidebar gradient styling to match hero section */
.side-bar {
background: linear-gradient(135deg, #ffffff 0%, rgba(20, 176, 20, 0.01) 25%, rgba(20, 176, 20, 0.04) 40%, rgba(220, 230, 255, 0.4) 60%, rgba(205, 218, 255, 0.4) 80%, #F5F6FA 100%);
}
</style>
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{{ site.url }}/feed.xml">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link rel="preload" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap" as="style" type="text/css" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Logo Progressive Enhancement for Jekyll pages -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const siteTitle = document.querySelector('.site-title');
if (siteTitle) {
const textContent = siteTitle.textContent; // Save the text for fallback
// Create new image element
const logoImg = new Image();
logoImg.src = '/assets/logo.svg';
logoImg.alt = 'Aider Logo';
logoImg.style.height = '1.8rem';
logoImg.style.verticalAlign = 'middle';
// Only replace if image loads successfully
logoImg.onload = function() {
siteTitle.textContent = ''; // Clear text
siteTitle.appendChild(logoImg);
};
// If image fails to load, do nothing (keep the text)
logoImg.onerror = function() {
console.log('SVG logo failed to load, keeping text fallback');
};
}
});
</script>
<meta name="theme-color" content="#157878">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" type="image/png" sizes="32x32" href="{{ '/assets/icons/favicon-32x32.png' | relative_url }}">

View file

@ -0,0 +1,5 @@
```bash
python -m pip install aider-install
aider-install
```

View file

@ -4,7 +4,11 @@ document.addEventListener('DOMContentLoaded', function () {
const redDiagonalPattern = pattern.draw('diagonal', 'rgba(255, 99, 132, 0.2)');
let displayedData = [];
const HIGHLIGHT_MODEL = '{{ highlight_model | default: "no no no" }}';
// Get highlight model from query string or Jekyll variable
const urlParams = new URLSearchParams(window.location.search);
const queryHighlight = urlParams.get('highlight');
const HIGHLIGHT_MODEL = queryHighlight || '{{ highlight_model | default: "no no no" }}';
var leaderboardData = {
labels: [],
datasets: [{
@ -13,14 +17,14 @@ document.addEventListener('DOMContentLoaded', function () {
backgroundColor: function(context) {
const row = allData[context.dataIndex];
if (row && row.edit_format === 'whole') {
return diagonalPattern;
return redDiagonalPattern; // Use red pattern for highlighted whole format
}
const label = leaderboardData.labels[context.dataIndex] || '';
return (label && label.includes(HIGHLIGHT_MODEL)) ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)';
return (label && HIGHLIGHT_MODEL && label.toLowerCase().includes(HIGHLIGHT_MODEL.toLowerCase())) ? 'rgba(255, 99, 132, 0.2)' : 'rgba(54, 162, 235, 0.2)';
},
borderColor: function(context) {
const label = context.chart.data.labels[context.dataIndex] || '';
return (label && label.includes(HIGHLIGHT_MODEL)) ? 'rgba(255, 99, 132, 1)' : 'rgba(54, 162, 235, 1)';
return (label && HIGHLIGHT_MODEL && label.toLowerCase().includes(HIGHLIGHT_MODEL.toLowerCase())) ? 'rgba(255, 99, 132, 1)' : 'rgba(54, 162, 235, 1)';
},
borderWidth: 1
}, {
@ -74,11 +78,13 @@ document.addEventListener('DOMContentLoaded', function () {
leaderboardChart.render();
}
// Use displayedData in the backgroundColor callback instead of allData
// Update backgroundColor and borderColor for the main dataset based on displayedData
leaderboardData.datasets[0].backgroundColor = function(context) {
const row = displayedData[context.dataIndex];
const label = leaderboardData.labels[context.dataIndex] || '';
if (label && label.includes(HIGHLIGHT_MODEL)) {
const isHighlighted = label && HIGHLIGHT_MODEL && label.toLowerCase().includes(HIGHLIGHT_MODEL.toLowerCase());
if (isHighlighted) {
if (row && row.edit_format === 'whole') return redDiagonalPattern;
else return 'rgba(255, 99, 132, 0.2)';
} else if (row && row.edit_format === 'whole') {
@ -171,6 +177,9 @@ document.addEventListener('DOMContentLoaded', function () {
},
x: {
ticks: {
autoSkip: false, // Prevent labels from being automatically skipped
maxRotation: 90, // Allow labels to rotate up to 90 degrees
minRotation: 0,
callback: function(value, index) {
const label = this.getLabelForValue(value);
if (label.length <= "claude-3-5-sonnet".length) {

View file

@ -0,0 +1,515 @@
document.addEventListener('DOMContentLoaded', function() {
let currentMode = 'view'; // 'view', 'select', 'detail'
let selectedRows = new Set(); // Store indices of selected rows
const MAX_DISPLAY_COST_CAP = 75; // Define the constant here
const allMainRows = document.querySelectorAll('tr[id^="main-row-"]');
const allDetailsRows = document.querySelectorAll('tr[id^="details-"]');
const searchInput = document.getElementById('editSearchInput');
const modeViewButton = document.getElementById('mode-view-btn');
const modeDetailButton = document.getElementById('mode-detail-btn');
const modeSelectButton = document.getElementById('mode-select-btn');
const modeButtons = [modeViewButton, modeSelectButton, modeDetailButton];
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const leaderboardTitle = document.getElementById('leaderboard-title'); // Get title element
const defaultTitle = "Aider polyglot coding leaderboard";
const filteredTitle = "Aider polyglot coding benchmark results (selected)";
function applySearchFilter() {
const searchTerm = searchInput.value.toLowerCase();
allMainRows.forEach(row => {
const textContent = row.textContent.toLowerCase();
const detailsRow = document.getElementById(row.id.replace('main-row-', 'details-'));
const matchesSearch = textContent.includes(searchTerm);
if (matchesSearch) {
row.classList.remove('hidden-by-search');
if (detailsRow) detailsRow.classList.remove('hidden-by-search');
} else {
row.classList.add('hidden-by-search');
if (detailsRow) detailsRow.classList.add('hidden-by-search');
}
});
// After applying search filter, re-apply view mode filter and update select-all state
updateTableView(currentMode);
if (currentMode === 'select') {
updateSelectAllCheckboxState();
}
// Update cost bars and ticks since visible rows may have changed
updateCostBars();
updateCostTicks();
}
function getVisibleMainRows() {
// Helper to get rows currently visible (not hidden by search or mode)
return Array.from(allMainRows).filter(row =>
!row.classList.contains('hidden-by-search') && !row.classList.contains('hidden-by-mode')
);
}
function updateSelectAllCheckboxState() {
// Update the header checkbox based on the selection state of *visible* rows
if (currentMode !== 'select') return; // Only relevant in select mode
const visibleRows = getVisibleMainRows();
const visibleRowCount = visibleRows.length;
const selectedVisibleRowCount = visibleRows.filter(row => selectedRows.has(row.querySelector('.row-selector')?.dataset.rowIndex)).length;
if (visibleRowCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (selectedVisibleRowCount === visibleRowCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (selectedVisibleRowCount > 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
}
function updateTableView(mode) {
currentMode = mode; // Update global state ('view', 'select', 'detail')
// Update button styles first
modeButtons.forEach(btn => {
btn.classList.remove('active');
// Reset specific styles potentially added by .active
btn.style.backgroundColor = '';
btn.style.color = '';
});
let activeButton;
if (mode === 'view') activeButton = modeViewButton;
else if (mode === 'select') activeButton = modeSelectButton;
else if (mode === 'detail') activeButton = modeDetailButton;
activeButton.classList.add('active');
activeButton.style.backgroundColor = '#e7f3ff'; // Use selected row highlight blue
activeButton.style.color = '#495057'; // Use dark text for contrast on light blue
// Get the first header cell (for the toggle/checkbox column)
const firstHeaderCell = document.querySelector('table thead th:first-child');
// Show/hide header checkbox based on mode
selectAllCheckbox.style.display = mode === 'select' ? 'inline-block' : 'none';
allMainRows.forEach(row => {
const rowIndex = row.querySelector('.row-selector')?.dataset.rowIndex;
const toggleButton = row.querySelector('.toggle-details');
const selectorCheckbox = row.querySelector('.row-selector');
const firstCell = row.querySelector('td:first-child'); // Get the first cell of the main row
const detailsRow = document.getElementById(`details-${rowIndex}`);
const isSelected = selectedRows.has(rowIndex);
// Reset visibility classes before applying mode logic
row.classList.remove('hidden-by-mode');
if (detailsRow) detailsRow.classList.remove('hidden-by-mode');
// Show/hide the first column (header and data cells) based on mode
if (firstHeaderCell) {
firstHeaderCell.style.display = mode === 'view' ? 'none' : '';
}
if (firstCell) {
firstCell.style.display = mode === 'view' ? 'none' : '';
}
// Apply mode-specific logic
if (mode === 'view') { // --- VIEW MODE ---
toggleButton.style.display = 'none'; // Hide toggle in view mode
selectorCheckbox.style.display = 'none';
row.classList.remove('row-selected'); // Ensure no selection highlight
// view-highlighted is handled by row click listener
// In 'view' mode, hide row if selections exist AND this row is NOT selected
if (selectedRows.size > 0 && !isSelected) {
row.classList.add('hidden-by-mode');
if (detailsRow) detailsRow.classList.add('hidden-by-mode');
} else {
// Ensure row is not hidden by mode if it's selected or no selections exist
// This is handled by the reset at the start of the loop:
// row.classList.remove('hidden-by-mode');
// if (detailsRow) detailsRow.classList.remove('hidden-by-mode');
}
// Always hide details row content in view mode regardless of visibility class
if (detailsRow) {
detailsRow.style.display = 'none';
}
} else if (mode === 'select') { // --- SELECT MODE ---
toggleButton.style.display = 'none';
selectorCheckbox.style.display = 'inline-block';
selectorCheckbox.checked = isSelected;
row.classList.toggle('row-selected', isSelected);
row.classList.remove('view-highlighted'); // Clear view highlight when switching to select
// Always hide details row in select mode
if (detailsRow) detailsRow.style.display = 'none';
// In 'select' mode, no rows should be hidden based on selection status
row.classList.remove('hidden-by-mode');
if (detailsRow) detailsRow.classList.remove('hidden-by-mode');
} else { // --- DETAIL MODE --- (mode === 'detail')
toggleButton.style.display = 'inline-block'; // Show toggle
selectorCheckbox.style.display = 'none';
row.classList.remove('row-selected'); // Clear selection highlight
row.classList.remove('view-highlighted'); // Clear view highlight when switching to detail
// Details row visibility is controlled by the toggle button state, don't force hide/show here
// Ensure main row is visible if not hidden by search
row.classList.remove('hidden-by-mode');
if (detailsRow) {
detailsRow.classList.remove('hidden-by-mode');
// Preserve existing display state (controlled by toggle) unless hidden by search
if (detailsRow.classList.contains('hidden-by-search')) {
detailsRow.style.display = 'none';
}
}
}
// Ensure rows hidden by search remain hidden regardless of mode
if (row.classList.contains('hidden-by-search')) {
row.style.display = 'none';
if (detailsRow) detailsRow.style.display = 'none';
} else if (!row.classList.contains('hidden-by-mode')) {
// Make row visible if not hidden by search or mode
row.style.display = ''; // Or 'table-row' if needed, but '' usually works
} else {
// Row is hidden by mode, ensure it's hidden
row.style.display = 'none';
if (detailsRow) detailsRow.style.display = 'none';
}
});
// Update the leaderboard title based on mode and selection
if (leaderboardTitle) {
if (currentMode === 'view' && selectedRows.size > 0) {
leaderboardTitle.textContent = filteredTitle;
} else {
leaderboardTitle.textContent = defaultTitle;
}
}
// Update the select-all checkbox state after updating the view
updateSelectAllCheckboxState();
// Update cost bars and ticks since visible/selected rows may have changed
updateCostBars();
updateCostTicks();
}
// --- Existing Initializations ---
// Add percentage ticks
const percentCells = document.querySelectorAll('.bar-cell:not(.cost-bar-cell)');
percentCells.forEach(cell => {
// Add ticks at 0%, 10%, 20%, ..., 100%
for (let i = 0; i <= 100; i += 10) {
const tick = document.createElement('div');
tick.className = 'percent-tick';
tick.style.left = `${i}%`;
cell.appendChild(tick);
}
});
// Function to calculate the appropriate max display cost based on visible/selected entries
function calculateDisplayMaxCost() {
// Get the appropriate set of rows based on the current mode and selection state
let rowsToConsider;
if (currentMode === 'view' && selectedRows.size > 0) {
// In view mode with selections, only consider selected rows
rowsToConsider = Array.from(allMainRows).filter(row => {
const rowIndex = row.querySelector('.row-selector')?.dataset.rowIndex;
return rowIndex && selectedRows.has(rowIndex) && !row.classList.contains('hidden-by-search');
});
} else {
// In other modes or without selections, consider all visible rows
rowsToConsider = getVisibleMainRows();
}
// Find the maximum cost among the rows to consider
let maxCost = 0;
rowsToConsider.forEach(row => {
const costBar = row.querySelector('.cost-bar');
if (costBar) {
const cost = parseFloat(costBar.dataset.cost || '0');
if (cost > maxCost) maxCost = cost;
}
});
// Cap at MAX_DISPLAY_COST_CAP if any entries exceed that amount, otherwise use actual max
return maxCost > MAX_DISPLAY_COST_CAP ? MAX_DISPLAY_COST_CAP : Math.max(1, maxCost); // Ensure at least 1 to avoid division by zero
}
// Process cost bars with dynamic scale
function updateCostBars() {
const costBars = document.querySelectorAll('.cost-bar');
const currentMaxDisplayCost = calculateDisplayMaxCost();
// Remove existing special indicators first
document.querySelectorAll('.dark-section, .tear-line').forEach(el => el.remove());
costBars.forEach(bar => {
const cost = parseFloat(bar.dataset.cost);
if (cost > 0) {
// Calculate percentage based on the dynamic display max
const percent = Math.min(cost, currentMaxDisplayCost) / currentMaxDisplayCost * 100;
// Clamp percentage between 0 and 100
bar.style.width = Math.max(0, Math.min(100, percent)) + '%';
// Mark bars that exceed the limit (only if our display max is capped at 50)
if (currentMaxDisplayCost === MAX_DISPLAY_COST_CAP && cost > MAX_DISPLAY_COST_CAP) {
// Create a darker section at the end with diagonal stripes
const darkSection = document.createElement('div');
darkSection.className = 'bar-viz dark-section';
darkSection.style.width = '15%'; // From 85% to 100%
darkSection.style.left = '85%';
darkSection.style.backgroundColor = 'rgba(13, 110, 253, 0.6)'; // Darker blue
darkSection.style.borderRight = '1px solid rgba(13, 110, 253, 0.8)';
darkSection.style.zIndex = '1';
// Add diagonal stripes with CSS background
darkSection.style.backgroundImage = 'repeating-linear-gradient(45deg, rgba(255,255,255,0.3), rgba(255,255,255,0.3) 5px, transparent 5px, transparent 10px)';
bar.parentNode.appendChild(darkSection);
// Add a dashed "tear line" at the transition point
const tearLine = document.createElement('div');
tearLine.className = 'tear-line';
tearLine.style.position = 'absolute';
tearLine.style.left = '85%';
// Center the tear line vertically and make it 1.5x as tall as the bar
tearLine.style.top = '50%';
tearLine.style.transform = 'translateY(-50%)';
tearLine.style.height = '54px'; // 1.5x the bar height (36px)
tearLine.style.width = '2px';
tearLine.style.backgroundColor = 'white';
tearLine.style.borderLeft = '2px dashed rgba(0, 0, 0, 0.3)';
tearLine.style.zIndex = '2'; // Above the bar
bar.parentNode.appendChild(tearLine);
}
} else {
// Set width to 0 if cost is 0 or negative
bar.style.width = '0%';
}
});
}
// Call this initially to set up the bars
updateCostBars();
// Update cost ticks dynamically based on current max display cost
function updateCostTicks() {
const costCells = document.querySelectorAll('.cost-bar-cell');
if (costCells.length === 0) return;
const currentMaxDisplayCost = calculateDisplayMaxCost();
// Remove existing ticks first
document.querySelectorAll('.cost-tick').forEach(tick => tick.remove());
// Generate appropriate tick values based on current max
let tickValues = [];
// Always use $10 increments, regardless of the max
const maxTickValue = Math.ceil(currentMaxDisplayCost / 10) * 10; // Round up to nearest $10
for (let i = 0; i <= maxTickValue; i += 10) {
tickValues.push(i);
}
// Calculate percentage positions for each tick
const tickPercentages = tickValues.map(tickCost => {
return (tickCost / currentMaxDisplayCost) * 100;
});
// Add tick divs to each cost cell
costCells.forEach(cell => {
const costBar = cell.querySelector('.cost-bar');
// Use optional chaining and provide '0' as fallback if costBar or dataset.cost is missing
const cost = parseFloat(costBar?.dataset?.cost || '0');
// Only add ticks if the cost is actually greater than 0
if (cost > 0) {
tickPercentages.forEach((percent, index) => {
// Ensure percentage is within valid range
if (percent >= 0 && percent <= 100) {
const tick = document.createElement('div');
tick.className = 'cost-tick';
tick.style.left = `${percent}%`;
cell.appendChild(tick);
}
});
}
});
}
// Call this initially to set up the ticks
updateCostTicks();
// --- New Event Listeners ---
// Listener for mode toggle buttons
modeButtons.forEach(button => {
button.addEventListener('click', function(event) {
const newMode = this.dataset.mode;
if (newMode !== currentMode) {
// Update active button style
modeButtons.forEach(btn => {
btn.classList.remove('active');
// Reset specific styles potentially added by .active
btn.style.backgroundColor = '';
btn.style.color = '';
});
this.classList.add('active');
// Apply active styles directly as inline styles might interfere
this.style.backgroundColor = '#e7f3ff'; // Use selected row highlight blue
this.style.color = '#495057'; // Use dark text for contrast on light blue
// Update table view and apply filters
updateTableView(newMode);
applySearchFilter(); // Re-apply search filter when mode changes
}
});
});
// Listener for row selector checkboxes (using event delegation on table body)
const tableBody = document.querySelector('table tbody');
tableBody.addEventListener('change', function(event) {
if (event.target.classList.contains('row-selector') && currentMode === 'select') {
const checkbox = event.target;
const rowIndex = checkbox.dataset.rowIndex;
const mainRow = checkbox.closest('tr');
if (checkbox.checked) {
selectedRows.add(rowIndex);
mainRow.classList.add('row-selected');
} else {
selectedRows.delete(rowIndex);
mainRow.classList.remove('row-selected');
}
// Update select-all checkbox state
updateSelectAllCheckboxState();
// Update cost bars and ticks if in view mode, as selection affects what's shown
if (currentMode === 'view') {
updateCostBars();
updateCostTicks();
}
}
}); // End of tableBody listener
// Listener for Select All checkbox
selectAllCheckbox.addEventListener('change', function() {
if (currentMode !== 'select') return;
const isChecked = selectAllCheckbox.checked;
// Select/deselect only the rows that are currently visible
const visibleRows = getVisibleMainRows();
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-selector');
const rowIndex = checkbox?.dataset.rowIndex;
if (!checkbox || !rowIndex) return; // Skip if no checkbox/index found
// Only change state if it differs from target state
if (checkbox.checked !== isChecked) {
checkbox.checked = isChecked;
row.classList.toggle('row-selected', isChecked);
if (isChecked) {
selectedRows.add(rowIndex);
} else {
selectedRows.delete(rowIndex);
}
}
});
// After bulk change, ensure the selectAll checkbox state is correct (not indeterminate)
updateSelectAllCheckboxState();
// Update cost bars and ticks after selection changes
updateCostBars();
updateCostTicks();
});
// Listener for search input
searchInput.addEventListener('input', applySearchFilter);
// Add toggle functionality for details (Modified to respect modes)
const toggleButtons = document.querySelectorAll('.toggle-details');
toggleButtons.forEach(button => {
button.addEventListener('click', function() {
// Only allow toggling in 'detail' mode
if (currentMode !== 'detail') return;
const targetId = this.getAttribute('data-target');
const targetRow = document.getElementById(targetId);
const mainRow = this.closest('tr'); // Get the main row associated with this button
if (targetRow && !mainRow.classList.contains('hidden-by-mode') && !mainRow.classList.contains('hidden-by-search')) {
const isVisible = targetRow.style.display !== 'none';
targetRow.style.display = isVisible ? 'none' : 'table-row';
this.textContent = isVisible ? '▶' : '▼';
}
});
});
// Listener for clicking anywhere on a row
tableBody.addEventListener('click', function(event) {
const clickedRow = event.target.closest('tr');
// Ensure it's a main row and not a details row or header/footer
if (!clickedRow || !clickedRow.id.startsWith('main-row-')) return;
// --- START conditional logic ---
if (currentMode === 'select') {
// --- SELECT MODE LOGIC (Existing) ---
// Find the checkbox within this row
const checkbox = clickedRow.querySelector('.row-selector');
if (!checkbox) return; // No checkbox found in this row
// If the click was directly on the checkbox or its label (if any),
// let the default behavior and the 'change' event listener handle it.
// Otherwise, toggle the checkbox state programmatically.
if (event.target !== checkbox && event.target.tagName !== 'LABEL' /* Add if you use labels */) {
checkbox.checked = !checkbox.checked;
// Manually trigger the change event to update state and UI
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
}
// --- END SELECT MODE LOGIC ---
} else if (currentMode === 'view') {
// --- VIEW MODE LOGIC (New) ---
// Don't highlight if the click was on the details toggle button
if (event.target.classList.contains('toggle-details')) {
return;
}
// Toggle the highlight class on the clicked row
clickedRow.classList.toggle('view-highlighted');
// --- END VIEW MODE LOGIC ---
}
// --- END conditional logic ---
});
// --- Initial Setup ---
updateTableView('view'); // Initialize view to 'view' mode
applySearchFilter(); // Apply initial search filter (if any text is pre-filled or just to set initial state)
// Close button functionality
const closeControlsBtn = document.getElementById('close-controls-btn');
if (closeControlsBtn) {
closeControlsBtn.addEventListener('click', function() {
const controlsContainer = document.getElementById('controls-container');
if (controlsContainer) {
controlsContainer.style.display = 'none';
}
});
}
});

View file

@ -4,7 +4,7 @@ You can send long, multi-line messages in the chat in a few ways:
- Or, start with `{tag` (where "tag" is any sequence of letters/numbers) and end with `tag}`. This is useful when you need to include closing braces `}` in your message.
- Use Meta-ENTER to start a new line without sending the message (Esc+ENTER in some environments).
- Use `/paste` to paste text from the clipboard into the chat.
- Use the `/editor` command to open your editor to create the next chat message. See [editor configuration docs](/docs/config/editor.html) for more info.
- Use the `/editor` command (or press `Ctrl-X Ctrl-E` if your terminal allows) to open your editor to create the next chat message. See [editor configuration docs](/docs/config/editor.html) for more info.
- Use multiline-mode, which swaps the function of Meta-Enter and Enter, so that Enter inserts a newline, and Meta-Enter submits your command. To enable multiline mode:
- Use the `/multiline-mode` command to toggle it during a session.
- Use the `--multiline` switch.

View file

@ -0,0 +1,228 @@
/* Terminal header styling */
.terminal-header {
background-color: #e0e0e0;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
padding: 4px 10px;
display: flex;
align-items: center;
border-bottom: 1px solid #c0c0c0;
}
.terminal-buttons {
display: flex;
gap: 4px;
margin-right: 10px;
}
.terminal-button {
width: 10px;
height: 10px;
border-radius: 50%;
}
.terminal-close {
background-color: #ff5f56;
border: 1px solid #e0443e;
}
.terminal-minimize {
background-color: #ffbd2e;
border: 1px solid #dea123;
}
.terminal-expand {
background-color: #27c93f;
border: 1px solid #1aab29;
}
.terminal-title {
flex-grow: 1;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 11px;
color: #666;
}
/* Toast notification styling */
.toast-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
pointer-events: none;
}
.toast-notification {
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 25px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.3s ease-in-out;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 18px;
text-align: center;
display: inline-block;
min-width: 200px;
max-width: 90%;
}
/* Page container styling */
.page-container {
max-width: 950px;
margin-left: auto;
margin-right: auto;
position: relative;
}
/* macOS backdrop styling */
.macos-backdrop {
background: linear-gradient(135deg, #ff9966, #ff5e62, #6666ff, #0066ff);
border-radius: 12px;
padding: clamp(5px, 5vw, 50px) clamp(5px, 2.5vw, 50px);
margin: 20px 0;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
/* Add subtle wave animation to backdrop */
.macos-backdrop::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
opacity: 0.7;
pointer-events: none;
}
/* Add decorative curved lines to the backdrop */
.macos-backdrop::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 20% 30%, transparent 0%, transparent 60%, rgba(255,255,255,0.2) 61%, transparent 62%),
radial-gradient(circle at 80% 70%, transparent 0%, transparent 40%, rgba(255,255,255,0.2) 41%, transparent 42%),
radial-gradient(circle at 40% 90%, transparent 0%, transparent 70%, rgba(255,255,255,0.2) 71%, transparent 72%),
radial-gradient(circle at 60% 10%, transparent 0%, transparent 50%, rgba(255,255,255,0.2) 51%, transparent 52%);
background-size: 100% 100%;
opacity: 1;
pointer-events: none;
z-index: 0;
}
.terminal-container {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
margin-top: 0;
margin-bottom: 0;
position: relative;
background-color: white; /* Add background color to terminal container */
z-index: 2; /* Ensure terminal appears above the backdrop effects */
}
/* Timestamp link styling */
.timestamp-link {
color: #0366d6;
text-decoration: none;
font-weight: bold;
cursor: pointer;
}
.timestamp-link:hover {
text-decoration: underline;
}
/* Active timestamp styling */
.timestamp-active {
background-color: #f0f8ff; /* Light blue background */
border-radius: 3px;
padding: 2px 4px;
margin: -2px -4px;
}
/* Highlight the list item containing the active timestamp */
li.active-marker {
background-color: #f6f8fa;
border-radius: 4px;
padding: 4px 8px;
margin-left: -8px;
}
/* Make list items clickable */
.transcript-item {
cursor: pointer;
transition: background-color 0.2s ease;
padding: 4px 8px;
margin-left: -8px;
border-radius: 4px;
}
.transcript-item:hover {
background-color: #f0f0f0;
}
/* Keyboard shortcuts styling */
.keyboard-shortcuts {
text-align: center;
font-size: 14px;
color: #666;
margin-top: 10px;
margin-bottom: 20px;
}
/* Hide keyboard shortcuts on devices likely without physical keyboards */
.no-physical-keyboard .keyboard-shortcuts {
display: none;
}
.keyboard-shortcuts kbd {
background-color: #f7f7f7;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 1px 0 rgba(0,0,0,0.2);
color: #333;
display: inline-block;
font-family: monospace;
line-height: 1;
margin: 0 2px;
padding: 3px 5px;
white-space: nowrap;
}
.asciinema-player-theme-aider {
/* Foreground (default text) color */
--term-color-foreground: #444444; /* colour238 */
/* Background color */
--term-color-background: #dadada; /* colour253 */
/* Palette of 16 standard ANSI colors */
--term-color-0: #21222c;
--term-color-1: #ff5555;
--term-color-2: #50fa7b;
--term-color-3: #f1fa8c;
--term-color-4: #bd93f9;
--term-color-5: #ff79c6;
--term-color-6: #8be9fd;
--term-color-7: #f8f8f2;
--term-color-8: #6272a4;
--term-color-9: #ff6e6e;
--term-color-10: #69ff94;
--term-color-11: #ffffa5;
--term-color-12: #d6acff;
--term-color-13: #ff92df;
--term-color-14: #a4ffff;
--term-color-15: #ffffff;
}

View file

@ -0,0 +1,428 @@
document.addEventListener('DOMContentLoaded', function() {
let player; // Store player reference to make it accessible to click handlers
let globalAudio; // Global audio element to be reused
// Detect if device likely has no physical keyboard
function detectNoKeyboard() {
// Check if it's a touch device (most mobile devices)
const isTouchDevice = ('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0);
// Check common mobile user agents as additional signal
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// If it's a touch device and has a mobile user agent, likely has no physical keyboard
if (isTouchDevice && isMobileUA) {
document.body.classList.add('no-physical-keyboard');
}
}
// Run detection
detectNoKeyboard();
// Parse the transcript section to create markers and convert timestamps to links
function parseTranscript() {
const markers = [];
// Find the Commentary heading
const transcriptHeading = Array.from(document.querySelectorAll('h2')).find(el => el.textContent.trim() === 'Commentary');
if (transcriptHeading) {
// Get all list items after the transcript heading
let currentElement = transcriptHeading.nextElementSibling;
while (currentElement && currentElement.tagName === 'UL') {
const listItems = currentElement.querySelectorAll('li');
listItems.forEach(item => {
const text = item.textContent.trim();
const match = text.match(/(\d+):(\d+)\s+(.*)/);
if (match) {
const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);
const timeInSeconds = minutes * 60 + seconds;
const formattedTime = `${minutes}:${seconds.toString().padStart(2, '0')}`;
const message = match[3].trim();
// Create link for the timestamp
const timeLink = document.createElement('a');
timeLink.href = '#';
timeLink.textContent = formattedTime;
timeLink.className = 'timestamp-link';
timeLink.dataset.time = timeInSeconds;
timeLink.dataset.message = message;
// Add click event to seek the player
timeLink.addEventListener('click', function(e) {
e.preventDefault();
if (player && typeof player.seek === 'function') {
player.seek(timeInSeconds);
player.play();
// Also trigger toast and speech
showToast(message);
speakText(message, timeInSeconds);
// Highlight this timestamp
highlightTimestamp(timeInSeconds);
}
});
// Replace text with the link + message
item.textContent = '';
item.appendChild(timeLink);
item.appendChild(document.createTextNode(' ' + message));
// Add class and click handler to the entire list item
item.classList.add('transcript-item');
item.dataset.time = timeInSeconds;
item.dataset.message = message;
item.addEventListener('click', function(e) {
// Prevent click event if the user clicked directly on the timestamp link
// This prevents double-firing of the event
if (e.target !== timeLink) {
e.preventDefault();
if (player && typeof player.seek === 'function') {
player.seek(timeInSeconds);
player.play();
// Also trigger toast and speech
showToast(message);
speakText(message, timeInSeconds);
// Highlight this timestamp
highlightTimestamp(timeInSeconds);
}
}
});
markers.push([timeInSeconds, message]);
}
});
currentElement = currentElement.nextElementSibling;
}
}
return markers;
}
// Parse transcript and create markers
const markers = parseTranscript();
// Create player with a single call
player = AsciinemaPlayer.create(
recording_url,
document.getElementById('demo'),
{
speed: 1.25,
idleTimeLimit: 1,
theme: "aider",
poster: "npt:0:01",
markers: markers,
controls: true
}
);
// Focus on the player element so keyboard shortcuts work immediately
setTimeout(() => {
// Use setTimeout to ensure the player is fully initialized
if (player && typeof player.focus === 'function') {
player.focus();
} else {
// If player doesn't have a focus method, try to find and focus the terminal element
const playerElement = document.querySelector('.asciinema-terminal');
if (playerElement) {
playerElement.focus();
} else {
// Last resort - try to find element with tabindex
const tabbableElement = document.querySelector('[tabindex]');
if (tabbableElement) {
tabbableElement.focus();
}
}
}
}, 100);
// Track active toast elements
let activeToast = null;
// Function to display toast notification
function showToast(text) {
// Get the appropriate container based on fullscreen state
let container = document.getElementById('toast-container');
const isFullscreen = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
// If in fullscreen, check if we need to create a fullscreen toast container
if (isFullscreen) {
// Target the fullscreen element as the container parent
const fullscreenElement = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
// Look for an existing fullscreen toast container
let fsContainer = fullscreenElement.querySelector('.fs-toast-container');
if (!fsContainer) {
// Create a new container for fullscreen mode
fsContainer = document.createElement('div');
fsContainer.className = 'toast-container fs-toast-container';
fsContainer.id = 'fs-toast-container';
fullscreenElement.appendChild(fsContainer);
}
container = fsContainer;
}
// Remove any existing toast
if (activeToast) {
hideToast(activeToast);
}
// Create toast element
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.textContent = text;
// Add to container
container.appendChild(toast);
// Store reference to active toast
activeToast = {
element: toast,
container: container
};
// Trigger animation
setTimeout(() => {
toast.style.opacity = '1';
}, 10);
return activeToast;
}
// Function to hide a toast
function hideToast(toastInfo) {
if (!toastInfo || !toastInfo.element) return;
toastInfo.element.style.opacity = '0';
setTimeout(() => {
if (toastInfo.container && toastInfo.container.contains(toastInfo.element)) {
toastInfo.container.removeChild(toastInfo.element);
}
// If this was the active toast, clear the reference
if (activeToast === toastInfo) {
activeToast = null;
}
}, 300); // Wait for fade out animation
}
// Track if TTS is currently in progress to prevent duplicates
let ttsInProgress = false;
let currentToast = null;
// Improved browser TTS function
function useBrowserTTS(text) {
// Don't start new speech if already in progress
if (ttsInProgress) {
console.log('Speech synthesis already in progress, skipping');
return false;
}
if ('speechSynthesis' in window) {
console.log('Using browser TTS fallback');
// Set flag to prevent duplicate speech
ttsInProgress = true;
// Cancel any ongoing speech
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.0;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// For iOS, use a shorter utterance if possible
if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
utterance.text = text.length > 100 ? text.substring(0, 100) + '...' : text;
}
utterance.onstart = () => console.log('Speech started');
utterance.onend = () => {
console.log('Speech ended');
ttsInProgress = false; // Reset flag when speech completes
// Hide toast when speech ends
if (currentToast) {
hideToast(currentToast);
currentToast = null;
}
};
utterance.onerror = (e) => {
console.warn('Speech error:', e);
ttsInProgress = false; // Reset flag on error
// Also hide toast on error
if (currentToast) {
hideToast(currentToast);
currentToast = null;
}
};
window.speechSynthesis.speak(utterance);
return true;
}
console.warn('SpeechSynthesis not supported');
return false;
}
// Function to play pre-generated TTS audio files
function speakText(text, timeInSeconds) {
// Show the toast and keep reference
currentToast = showToast(text);
// Format time for filename (MM-SS)
const minutes = Math.floor(timeInSeconds / 60);
const seconds = timeInSeconds % 60;
const formattedTime = `${minutes.toString().padStart(2, '0')}-${seconds.toString().padStart(2, '0')}`;
// Get recording_id from the page or use default from the URL
const recordingId = typeof recording_id !== 'undefined' ? recording_id :
window.location.pathname.split('/').pop().replace('.html', '');
// Construct audio file path
const audioPath = `/assets/audio/${recordingId}/${formattedTime}.mp3`;
// Log for debugging
console.log(`Attempting to play audio: ${audioPath}`);
// Detect iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
console.log(`Device is iOS: ${isIOS}`);
// Flag to track if we've already fallen back to TTS
let fallenBackToTTS = false;
try {
// Create or reuse audio element
if (!globalAudio) {
globalAudio = new Audio();
console.log("Created new global Audio element");
}
// Set up event handlers
globalAudio.onended = () => {
console.log('Audio playback ended');
// Hide toast when audio ends
if (currentToast) {
hideToast(currentToast);
currentToast = null;
}
};
globalAudio.onerror = (e) => {
console.warn(`Audio error: ${e.type}`, e);
if (!fallenBackToTTS) {
fallenBackToTTS = true;
useBrowserTTS(text);
} else if (currentToast) {
// If we've already tried TTS and that failed too, hide the toast
hideToast(currentToast);
currentToast = null;
}
};
// For iOS, preload might help with subsequent plays
if (isIOS) {
globalAudio.preload = "auto";
}
// Set the new source
globalAudio.src = audioPath;
// Play with proper error handling
const playPromise = globalAudio.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
console.warn(`Play error: ${error.message}`);
// On iOS, a user gesture might be required
if (isIOS) {
console.log("iOS playback failed, trying SpeechSynthesis");
}
if (!fallenBackToTTS) {
fallenBackToTTS = true;
useBrowserTTS(text);
}
});
}
} catch (e) {
console.error(`Exception in audio playback: ${e.message}`);
useBrowserTTS(text);
}
}
// Function to highlight the active timestamp in the transcript
function highlightTimestamp(timeInSeconds) {
// Remove previous highlights
document.querySelectorAll('.timestamp-active').forEach(el => {
el.classList.remove('timestamp-active');
});
document.querySelectorAll('.active-marker').forEach(el => {
el.classList.remove('active-marker');
});
// Find the timestamp link with matching time
const timestampLinks = document.querySelectorAll('.timestamp-link');
let activeLink = null;
for (const link of timestampLinks) {
if (parseInt(link.dataset.time) === timeInSeconds) {
activeLink = link;
break;
}
}
if (activeLink) {
// Add highlight class to the link
activeLink.classList.add('timestamp-active');
// Also highlight the parent list item
const listItem = activeLink.closest('li');
if (listItem) {
listItem.classList.add('active-marker');
// No longer scrolling into view to avoid shifting focus
}
}
}
// Add event listener with safety checks
if (player && typeof player.addEventListener === 'function') {
player.addEventListener('marker', function(event) {
try {
const { index, time, label } = event;
console.log(`marker! ${index} - ${time} - ${label}`);
// Speak the marker label (toast is now shown within speakText)
speakText(label, time);
// Highlight the corresponding timestamp in the transcript
highlightTimestamp(time);
} catch (error) {
console.error('Error in marker event handler:', error);
}
});
}
});

View file

@ -0,0 +1,34 @@
<link rel="stylesheet" type="text/css" href="/assets/asciinema/asciinema-player.css" />
<style>
{% include recording.css %}
</style>
<script src="/assets/asciinema/asciinema-player.min.js"></script>
<script>
{% include recording.js %}
</script>
<div class="page-container">
<div class="toast-container" id="toast-container"></div>
<div class="macos-backdrop">
<div class="terminal-container">
<div class="terminal-header">
<div class="terminal-buttons">
<div class="terminal-button terminal-close"></div>
<div class="terminal-button terminal-minimize"></div>
<div class="terminal-button terminal-expand"></div>
</div>
<div class="terminal-title">aider</div>
</div>
<div id="demo"></div>
</div>
</div>
</div>
<div class="keyboard-shortcuts">
<kbd>Space</kbd> Play/pause —
<kbd>f</kbd> Fullscreen —
<kbd></kbd><kbd></kbd> ±5s
</div>

View file

@ -39,9 +39,7 @@ Aider will directly edit the code in your local source files,
and [git commit the changes](https://aider.chat/docs/git.html)
with sensible commit messages.
You can start a new project or work with an existing git repo.
Aider works well with GPT 3.5, GPT-4, GPT-4 Turbo with Vision,
and Claude 3 Opus.
It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.html).
{% include works-best.md %}
Use the `--browser` switch to launch the browser version of aider:

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
{
"00-01": "We're going to add a new feature to automatically accept edits proposed by the architect model.",
"00-11": "First, let's add the new switch.",
"00-40": "Aider figured out that it should be passed to the Coder class.",
"00-48": "Now we need to implement the functionality.",
"01-00": "Let's do some manual testing.",
"01-28": "That worked. Let's make sure we can turn it off too.",
"02-00": "Let's quickly tidy up the changes to HISTORY.",
"02-05": "All done!",
"01-42": "That worked too. Let's have aider update the HISTORY file to document the new feature."
}

Some files were not shown because too many files have changed in this diff Show more