aider-ce 0.88.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +34 -0
  4. aider/analytics.py +258 -0
  5. aider/args.py +1056 -0
  6. aider/args_formatter.py +228 -0
  7. aider/change_tracker.py +133 -0
  8. aider/coders/__init__.py +36 -0
  9. aider/coders/agent_coder.py +2166 -0
  10. aider/coders/agent_prompts.py +104 -0
  11. aider/coders/architect_coder.py +48 -0
  12. aider/coders/architect_prompts.py +40 -0
  13. aider/coders/ask_coder.py +9 -0
  14. aider/coders/ask_prompts.py +35 -0
  15. aider/coders/base_coder.py +3613 -0
  16. aider/coders/base_prompts.py +87 -0
  17. aider/coders/chat_chunks.py +64 -0
  18. aider/coders/context_coder.py +53 -0
  19. aider/coders/context_prompts.py +75 -0
  20. aider/coders/editblock_coder.py +657 -0
  21. aider/coders/editblock_fenced_coder.py +10 -0
  22. aider/coders/editblock_fenced_prompts.py +143 -0
  23. aider/coders/editblock_func_coder.py +141 -0
  24. aider/coders/editblock_func_prompts.py +27 -0
  25. aider/coders/editblock_prompts.py +175 -0
  26. aider/coders/editor_diff_fenced_coder.py +9 -0
  27. aider/coders/editor_diff_fenced_prompts.py +11 -0
  28. aider/coders/editor_editblock_coder.py +9 -0
  29. aider/coders/editor_editblock_prompts.py +21 -0
  30. aider/coders/editor_whole_coder.py +9 -0
  31. aider/coders/editor_whole_prompts.py +12 -0
  32. aider/coders/help_coder.py +16 -0
  33. aider/coders/help_prompts.py +46 -0
  34. aider/coders/patch_coder.py +706 -0
  35. aider/coders/patch_prompts.py +159 -0
  36. aider/coders/search_replace.py +757 -0
  37. aider/coders/shell.py +37 -0
  38. aider/coders/single_wholefile_func_coder.py +102 -0
  39. aider/coders/single_wholefile_func_prompts.py +27 -0
  40. aider/coders/udiff_coder.py +429 -0
  41. aider/coders/udiff_prompts.py +115 -0
  42. aider/coders/udiff_simple.py +14 -0
  43. aider/coders/udiff_simple_prompts.py +25 -0
  44. aider/coders/wholefile_coder.py +144 -0
  45. aider/coders/wholefile_func_coder.py +134 -0
  46. aider/coders/wholefile_func_prompts.py +27 -0
  47. aider/coders/wholefile_prompts.py +65 -0
  48. aider/commands.py +2173 -0
  49. aider/copypaste.py +72 -0
  50. aider/deprecated.py +126 -0
  51. aider/diffs.py +128 -0
  52. aider/dump.py +29 -0
  53. aider/editor.py +147 -0
  54. aider/exceptions.py +115 -0
  55. aider/format_settings.py +26 -0
  56. aider/gui.py +545 -0
  57. aider/help.py +163 -0
  58. aider/help_pats.py +19 -0
  59. aider/helpers/__init__.py +9 -0
  60. aider/helpers/similarity.py +98 -0
  61. aider/history.py +180 -0
  62. aider/io.py +1608 -0
  63. aider/linter.py +304 -0
  64. aider/llm.py +55 -0
  65. aider/main.py +1415 -0
  66. aider/mcp/__init__.py +174 -0
  67. aider/mcp/server.py +149 -0
  68. aider/mdstream.py +243 -0
  69. aider/models.py +1313 -0
  70. aider/onboarding.py +429 -0
  71. aider/openrouter.py +129 -0
  72. aider/prompts.py +56 -0
  73. aider/queries/tree-sitter-language-pack/README.md +7 -0
  74. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  78. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  79. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  80. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  82. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  83. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  84. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  85. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  86. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  87. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  88. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  89. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  90. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  91. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  92. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  93. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  94. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  95. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  96. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  97. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  98. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  99. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  100. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  101. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  102. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  103. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  104. aider/queries/tree-sitter-languages/README.md +24 -0
  105. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  106. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  107. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  108. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  109. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  110. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  111. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  112. aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  113. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  114. aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  115. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  116. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  117. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  118. aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
  119. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  120. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  121. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  122. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  123. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  124. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  125. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  126. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  127. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  128. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  129. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  130. aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
  131. aider/reasoning_tags.py +82 -0
  132. aider/repo.py +621 -0
  133. aider/repomap.py +1174 -0
  134. aider/report.py +260 -0
  135. aider/resources/__init__.py +3 -0
  136. aider/resources/model-metadata.json +776 -0
  137. aider/resources/model-settings.yml +2068 -0
  138. aider/run_cmd.py +133 -0
  139. aider/scrape.py +293 -0
  140. aider/sendchat.py +242 -0
  141. aider/sessions.py +256 -0
  142. aider/special.py +203 -0
  143. aider/tools/__init__.py +72 -0
  144. aider/tools/command.py +105 -0
  145. aider/tools/command_interactive.py +122 -0
  146. aider/tools/delete_block.py +182 -0
  147. aider/tools/delete_line.py +155 -0
  148. aider/tools/delete_lines.py +184 -0
  149. aider/tools/extract_lines.py +341 -0
  150. aider/tools/finished.py +48 -0
  151. aider/tools/git_branch.py +129 -0
  152. aider/tools/git_diff.py +60 -0
  153. aider/tools/git_log.py +57 -0
  154. aider/tools/git_remote.py +53 -0
  155. aider/tools/git_show.py +51 -0
  156. aider/tools/git_status.py +46 -0
  157. aider/tools/grep.py +256 -0
  158. aider/tools/indent_lines.py +221 -0
  159. aider/tools/insert_block.py +288 -0
  160. aider/tools/list_changes.py +86 -0
  161. aider/tools/ls.py +93 -0
  162. aider/tools/make_editable.py +85 -0
  163. aider/tools/make_readonly.py +69 -0
  164. aider/tools/remove.py +91 -0
  165. aider/tools/replace_all.py +126 -0
  166. aider/tools/replace_line.py +173 -0
  167. aider/tools/replace_lines.py +217 -0
  168. aider/tools/replace_text.py +187 -0
  169. aider/tools/show_numbered_context.py +147 -0
  170. aider/tools/tool_utils.py +313 -0
  171. aider/tools/undo_change.py +95 -0
  172. aider/tools/update_todo_list.py +156 -0
  173. aider/tools/view.py +57 -0
  174. aider/tools/view_files_matching.py +141 -0
  175. aider/tools/view_files_with_symbol.py +129 -0
  176. aider/urls.py +17 -0
  177. aider/utils.py +456 -0
  178. aider/versioncheck.py +113 -0
  179. aider/voice.py +205 -0
  180. aider/waiting.py +38 -0
  181. aider/watch.py +318 -0
  182. aider/watch_prompts.py +12 -0
  183. aider/website/Gemfile +8 -0
  184. aider/website/_includes/blame.md +162 -0
  185. aider/website/_includes/get-started.md +22 -0
  186. aider/website/_includes/help-tip.md +5 -0
  187. aider/website/_includes/help.md +24 -0
  188. aider/website/_includes/install.md +5 -0
  189. aider/website/_includes/keys.md +4 -0
  190. aider/website/_includes/model-warnings.md +67 -0
  191. aider/website/_includes/multi-line.md +22 -0
  192. aider/website/_includes/python-m-aider.md +5 -0
  193. aider/website/_includes/recording.css +228 -0
  194. aider/website/_includes/recording.md +34 -0
  195. aider/website/_includes/replit-pipx.md +9 -0
  196. aider/website/_includes/works-best.md +1 -0
  197. aider/website/_sass/custom/custom.scss +103 -0
  198. aider/website/docs/config/adv-model-settings.md +2261 -0
  199. aider/website/docs/config/agent-mode.md +194 -0
  200. aider/website/docs/config/aider_conf.md +548 -0
  201. aider/website/docs/config/api-keys.md +90 -0
  202. aider/website/docs/config/dotenv.md +493 -0
  203. aider/website/docs/config/editor.md +127 -0
  204. aider/website/docs/config/mcp.md +95 -0
  205. aider/website/docs/config/model-aliases.md +104 -0
  206. aider/website/docs/config/options.md +890 -0
  207. aider/website/docs/config/reasoning.md +210 -0
  208. aider/website/docs/config.md +44 -0
  209. aider/website/docs/faq.md +384 -0
  210. aider/website/docs/git.md +76 -0
  211. aider/website/docs/index.md +47 -0
  212. aider/website/docs/install/codespaces.md +39 -0
  213. aider/website/docs/install/docker.md +57 -0
  214. aider/website/docs/install/optional.md +100 -0
  215. aider/website/docs/install/replit.md +8 -0
  216. aider/website/docs/install.md +115 -0
  217. aider/website/docs/languages.md +264 -0
  218. aider/website/docs/legal/contributor-agreement.md +111 -0
  219. aider/website/docs/legal/privacy.md +104 -0
  220. aider/website/docs/llms/anthropic.md +77 -0
  221. aider/website/docs/llms/azure.md +48 -0
  222. aider/website/docs/llms/bedrock.md +132 -0
  223. aider/website/docs/llms/cohere.md +34 -0
  224. aider/website/docs/llms/deepseek.md +32 -0
  225. aider/website/docs/llms/gemini.md +49 -0
  226. aider/website/docs/llms/github.md +111 -0
  227. aider/website/docs/llms/groq.md +36 -0
  228. aider/website/docs/llms/lm-studio.md +39 -0
  229. aider/website/docs/llms/ollama.md +75 -0
  230. aider/website/docs/llms/openai-compat.md +39 -0
  231. aider/website/docs/llms/openai.md +58 -0
  232. aider/website/docs/llms/openrouter.md +78 -0
  233. aider/website/docs/llms/other.md +117 -0
  234. aider/website/docs/llms/vertex.md +50 -0
  235. aider/website/docs/llms/warnings.md +10 -0
  236. aider/website/docs/llms/xai.md +53 -0
  237. aider/website/docs/llms.md +54 -0
  238. aider/website/docs/more/analytics.md +127 -0
  239. aider/website/docs/more/edit-formats.md +116 -0
  240. aider/website/docs/more/infinite-output.md +165 -0
  241. aider/website/docs/more-info.md +8 -0
  242. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  243. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  244. aider/website/docs/recordings/index.md +21 -0
  245. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  246. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  247. aider/website/docs/repomap.md +112 -0
  248. aider/website/docs/scripting.md +100 -0
  249. aider/website/docs/sessions.md +203 -0
  250. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  251. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  252. aider/website/docs/troubleshooting/imports.md +62 -0
  253. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  254. aider/website/docs/troubleshooting/support.md +79 -0
  255. aider/website/docs/troubleshooting/token-limits.md +96 -0
  256. aider/website/docs/troubleshooting/warnings.md +12 -0
  257. aider/website/docs/troubleshooting.md +11 -0
  258. aider/website/docs/usage/browser.md +57 -0
  259. aider/website/docs/usage/caching.md +49 -0
  260. aider/website/docs/usage/commands.md +133 -0
  261. aider/website/docs/usage/conventions.md +119 -0
  262. aider/website/docs/usage/copypaste.md +121 -0
  263. aider/website/docs/usage/images-urls.md +48 -0
  264. aider/website/docs/usage/lint-test.md +118 -0
  265. aider/website/docs/usage/modes.md +211 -0
  266. aider/website/docs/usage/not-code.md +179 -0
  267. aider/website/docs/usage/notifications.md +87 -0
  268. aider/website/docs/usage/tips.md +79 -0
  269. aider/website/docs/usage/tutorials.md +30 -0
  270. aider/website/docs/usage/voice.md +121 -0
  271. aider/website/docs/usage/watch.md +294 -0
  272. aider/website/docs/usage.md +102 -0
  273. aider/website/share/index.md +101 -0
  274. aider_ce-0.88.20.dist-info/METADATA +187 -0
  275. aider_ce-0.88.20.dist-info/RECORD +279 -0
  276. aider_ce-0.88.20.dist-info/WHEEL +5 -0
  277. aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
  278. aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
  279. aider_ce-0.88.20.dist-info/top_level.txt +1 -0
aider/onboarding.py ADDED
@@ -0,0 +1,429 @@
1
+ import base64
2
+ import hashlib
3
+ import http.server
4
+ import os
5
+ import secrets
6
+ import socketserver
7
+ import threading
8
+ import time
9
+ import webbrowser
10
+ from urllib.parse import parse_qs, urlparse
11
+
12
+ import requests
13
+
14
+ from aider import urls
15
+ from aider.io import InputOutput
16
+
17
+
18
+ def check_openrouter_tier(api_key):
19
+ """
20
+ Checks if the user is on a free tier for OpenRouter.
21
+
22
+ Args:
23
+ api_key: The OpenRouter API key to check.
24
+
25
+ Returns:
26
+ A boolean indicating if the user is on a free tier (True) or paid tier (False).
27
+ Returns True if the check fails.
28
+ """
29
+ try:
30
+ response = requests.get(
31
+ "https://openrouter.ai/api/v1/auth/key",
32
+ headers={"Authorization": f"Bearer {api_key}"},
33
+ timeout=5, # Add a reasonable timeout
34
+ )
35
+ response.raise_for_status()
36
+ data = response.json()
37
+ # According to the documentation, 'is_free_tier' will be true if the user has never paid
38
+ return data.get("data", {}).get("is_free_tier", True) # Default to True if not found
39
+ except Exception:
40
+ # If there's any error, we'll default to assuming free tier
41
+ return True
42
+
43
+
44
+ def try_to_select_default_model():
45
+ """
46
+ Attempts to select a default model based on available API keys.
47
+ Checks OpenRouter tier status to select appropriate model.
48
+
49
+ Returns:
50
+ The name of the selected model, or None if no suitable default is found.
51
+ """
52
+ # Special handling for OpenRouter
53
+ openrouter_key = os.environ.get("OPENROUTER_API_KEY")
54
+ if openrouter_key:
55
+ # Check if the user is on a free tier
56
+ is_free_tier = check_openrouter_tier(openrouter_key)
57
+ if is_free_tier:
58
+ return "openrouter/deepseek/deepseek-r1:free"
59
+ else:
60
+ return "openrouter/anthropic/claude-sonnet-4"
61
+
62
+ # Select model based on other available API keys
63
+ model_key_pairs = [
64
+ ("ANTHROPIC_API_KEY", "sonnet"),
65
+ ("DEEPSEEK_API_KEY", "deepseek"),
66
+ ("OPENAI_API_KEY", "gpt-4o"),
67
+ ("GEMINI_API_KEY", "gemini/gemini-2.5-pro-exp-03-25"),
68
+ ("VERTEXAI_PROJECT", "vertex_ai/gemini-2.5-pro-exp-03-25"),
69
+ ]
70
+
71
+ for env_key, model_name in model_key_pairs:
72
+ api_key_value = os.environ.get(env_key)
73
+ if api_key_value:
74
+ return model_name
75
+
76
+ return None
77
+
78
+
79
+ async def offer_openrouter_oauth(io, analytics):
80
+ """
81
+ Offers OpenRouter OAuth flow to the user if no API keys are found.
82
+
83
+ Args:
84
+ io: The InputOutput object for user interaction.
85
+ analytics: The Analytics object for tracking events.
86
+
87
+ Returns:
88
+ True if authentication was successful, False otherwise.
89
+ """
90
+ # No API keys found - Offer OpenRouter OAuth
91
+ io.tool_output("OpenRouter provides free and paid access to many LLMs.")
92
+ # Use confirm_ask which handles non-interactive cases
93
+ if await io.confirm_ask(
94
+ "Login to OpenRouter or create a free account?",
95
+ default="y",
96
+ acknowledge=True,
97
+ ):
98
+ analytics.event("oauth_flow_initiated", provider="openrouter")
99
+ openrouter_key = start_openrouter_oauth_flow(io, analytics)
100
+ if openrouter_key:
101
+ # Successfully got key via OAuth, use the default OpenRouter model
102
+ # Ensure OPENROUTER_API_KEY is now set in the environment for later use
103
+ os.environ["OPENROUTER_API_KEY"] = openrouter_key
104
+ # Track OAuth success leading to model selection
105
+ analytics.event("oauth_flow_success")
106
+ return True
107
+
108
+ # OAuth failed or was cancelled by user implicitly (e.g., closing browser)
109
+ # Error messages are handled within start_openrouter_oauth_flow
110
+ analytics.event("oauth_flow_failure")
111
+ io.tool_error("OpenRouter authentication did not complete successfully.")
112
+ # Fall through to the final error message
113
+
114
+ return False
115
+
116
+
117
+ async def select_default_model(args, io, analytics):
118
+ """
119
+ Selects a default model based on available API keys if no model is specified.
120
+ Offers OAuth flow for OpenRouter if no keys are found.
121
+
122
+ Args:
123
+ args: The command line arguments object.
124
+ io: The InputOutput object for user interaction.
125
+ analytics: The Analytics object for tracking events.
126
+
127
+ Returns:
128
+ The name of the selected model, or None if no suitable default is found.
129
+ """
130
+ if args.model:
131
+ return args.model # Model already specified
132
+
133
+ model = try_to_select_default_model()
134
+ if model:
135
+ io.tool_warning(f"Using {model} model with API key from environment.")
136
+ analytics.event("auto_model_selection", model=model)
137
+ return model
138
+
139
+ no_model_msg = "No LLM model was specified and no API keys were provided."
140
+ io.tool_warning(no_model_msg)
141
+
142
+ # Try OAuth if no model was detected
143
+ await offer_openrouter_oauth(io, analytics)
144
+
145
+ # Check again after potential OAuth success
146
+ model = try_to_select_default_model()
147
+ if model:
148
+ return model
149
+
150
+ await io.offer_url(urls.models_and_keys, "Open documentation URL for more info?")
151
+
152
+
153
+ # Helper function to find an available port
154
+ def find_available_port(start_port=8484, end_port=8584):
155
+ for port in range(start_port, end_port + 1):
156
+ try:
157
+ # Check if the port is available by trying to bind to it
158
+ with socketserver.TCPServer(("localhost", port), None):
159
+ return port
160
+ except OSError:
161
+ # Port is likely already in use
162
+ continue
163
+ return None
164
+
165
+
166
+ # PKCE code generation
167
+ def generate_pkce_codes():
168
+ code_verifier = secrets.token_urlsafe(64)
169
+ hasher = hashlib.sha256()
170
+ hasher.update(code_verifier.encode("utf-8"))
171
+ code_challenge = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=").decode("utf-8")
172
+ return code_verifier, code_challenge
173
+
174
+
175
+ # Function to exchange the authorization code for an API key
176
+ def exchange_code_for_key(code, code_verifier, io):
177
+ try:
178
+ response = requests.post(
179
+ "https://openrouter.ai/api/v1/auth/keys",
180
+ headers={"Content-Type": "application/json"},
181
+ json={
182
+ "code": code,
183
+ "code_verifier": code_verifier,
184
+ "code_challenge_method": "S256",
185
+ },
186
+ timeout=30, # Add a timeout
187
+ )
188
+ response.raise_for_status() # Raise exception for bad status codes (4xx or 5xx)
189
+ data = response.json()
190
+ api_key = data.get("key")
191
+ if not api_key:
192
+ io.tool_error("Error: 'key' not found in OpenRouter response.")
193
+ io.tool_error(f"Response: {response.text}")
194
+ return None
195
+ return api_key
196
+ except requests.exceptions.Timeout:
197
+ io.tool_error("Error: Request to OpenRouter timed out during code exchange.")
198
+ return None
199
+ except requests.exceptions.HTTPError as e:
200
+ io.tool_error(
201
+ "Error exchanging code for OpenRouter key:"
202
+ f" {e.response.status_code} {e.response.reason}"
203
+ )
204
+ io.tool_error(f"Response: {e.response.text}")
205
+ return None
206
+ except requests.exceptions.RequestException as e:
207
+ io.tool_error(f"Error exchanging code for OpenRouter key: {e}")
208
+ return None
209
+ except Exception as e:
210
+ io.tool_error(f"Unexpected error during code exchange: {e}")
211
+ return None
212
+
213
+
214
+ # Function to start the OAuth flow
215
+ def start_openrouter_oauth_flow(io, analytics):
216
+ """Initiates the OpenRouter OAuth PKCE flow using a local server."""
217
+
218
+ port = find_available_port()
219
+ if not port:
220
+ io.tool_error("Could not find an available port between 8484 and 8584.")
221
+ io.tool_error("Please ensure a port in this range is free, or configure manually.")
222
+ return None
223
+
224
+ callback_url = f"http://localhost:{port}/callback/aider"
225
+ auth_code = None
226
+ server_error = None
227
+ server_started = threading.Event()
228
+ shutdown_server = threading.Event()
229
+
230
+ class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler):
231
+ def do_GET(self):
232
+ nonlocal auth_code, server_error
233
+ parsed_path = urlparse(self.path)
234
+ if parsed_path.path == "/callback/aider":
235
+ query_params = parse_qs(parsed_path.query)
236
+ if "code" in query_params:
237
+ auth_code = query_params["code"][0]
238
+ self.send_response(200)
239
+ self.send_header("Content-type", "text/html")
240
+ self.end_headers()
241
+ self.wfile.write(
242
+ b"<html><body><h1>Success!</h1>"
243
+ b"<p>Aider has received the authentication code. "
244
+ b"You can close this browser tab.</p></body></html>"
245
+ )
246
+ # Signal the main thread to shut down the server
247
+ # Signal the main thread to shut down the server
248
+ shutdown_server.set()
249
+ else:
250
+ # Redirect to aider website if 'code' is missing (e.g., user visited manually)
251
+ self.send_response(302) # Found (temporary redirect)
252
+ self.send_header("Location", urls.website)
253
+ self.end_headers()
254
+ # No need to set server_error, just redirect.
255
+ # Do NOT shut down the server here; wait for timeout or success.
256
+ else:
257
+ # Redirect anything else (e.g., favicon.ico) to the main website as well
258
+ self.send_response(302)
259
+ self.send_header("Location", urls.website)
260
+ self.end_headers()
261
+ self.wfile.write(b"Not Found")
262
+
263
+ def log_message(self, format, *args):
264
+ # Suppress server logging to keep terminal clean
265
+ pass
266
+
267
+ def run_server():
268
+ nonlocal server_error
269
+ try:
270
+ with socketserver.TCPServer(("localhost", port), OAuthCallbackHandler) as httpd:
271
+ io.tool_output(f"Temporary server listening on {callback_url}", log_only=True)
272
+ server_started.set() # Signal that the server is ready
273
+ # Wait until shutdown is requested or timeout occurs (handled by main thread)
274
+ while not shutdown_server.is_set():
275
+ httpd.handle_request() # Handle one request at a time
276
+ # Add a small sleep to prevent busy-waiting if needed,
277
+ # though handle_request should block appropriately.
278
+ time.sleep(0.1)
279
+ io.tool_output("Shutting down temporary server.", log_only=True)
280
+ except Exception as e:
281
+ server_error = f"Failed to start or run temporary server: {e}"
282
+ server_started.set() # Signal even if failed, error will be checked
283
+ shutdown_server.set() # Ensure shutdown logic proceeds
284
+
285
+ server_thread = threading.Thread(target=run_server, daemon=True)
286
+ server_thread.start()
287
+
288
+ # Wait briefly for the server to start, or for an error
289
+ if not server_started.wait(timeout=5):
290
+ io.tool_error("Temporary authentication server failed to start in time.")
291
+ shutdown_server.set() # Ensure thread exits if it eventually starts
292
+ server_thread.join(timeout=1)
293
+ return None
294
+
295
+ # Check if server failed during startup
296
+ if server_error:
297
+ io.tool_error(server_error)
298
+ shutdown_server.set() # Ensure thread exits
299
+ server_thread.join(timeout=1)
300
+ return None
301
+
302
+ # Generate codes and URL
303
+ code_verifier, code_challenge = generate_pkce_codes()
304
+ auth_url_base = "https://openrouter.ai/auth"
305
+ auth_params = {
306
+ "callback_url": callback_url,
307
+ "code_challenge": code_challenge,
308
+ "code_challenge_method": "S256",
309
+ }
310
+ auth_url = f"{auth_url_base}?{'&'.join(f'{k}={v}' for k, v in auth_params.items())}"
311
+
312
+ io.tool_output("\nPlease open this URL in your browser to connect Aider with OpenRouter:")
313
+ io.tool_output()
314
+ print(auth_url)
315
+
316
+ MINUTES = 5
317
+ io.tool_output(f"\nWaiting up to {MINUTES} minutes for you to finish in the browser...")
318
+ io.tool_output("Use Control-C to interrupt.")
319
+
320
+ try:
321
+ webbrowser.open(auth_url)
322
+ except Exception:
323
+ pass
324
+
325
+ # Wait for the callback to set the auth_code or for timeout/error
326
+ interrupted = False
327
+ try:
328
+ shutdown_server.wait(timeout=MINUTES * 60) # Convert minutes to seconds
329
+ except KeyboardInterrupt:
330
+ io.tool_warning("\nOAuth flow interrupted.")
331
+ analytics.event("oauth_flow_failed", provider="openrouter", reason="user_interrupt")
332
+ interrupted = True
333
+ # Ensure the server thread is signaled to shut down
334
+ shutdown_server.set()
335
+
336
+ # Join the server thread to ensure it's cleaned up
337
+ server_thread.join(timeout=1)
338
+
339
+ if interrupted:
340
+ return None # Return None if interrupted by user
341
+
342
+ if server_error:
343
+ io.tool_error(f"Authentication failed: {server_error}")
344
+ analytics.event("oauth_flow_failed", provider="openrouter", reason=server_error)
345
+ return None
346
+
347
+ if not auth_code:
348
+ io.tool_error("Authentication with OpenRouter failed.")
349
+ analytics.event("oauth_flow_failed", provider="openrouter")
350
+ return None
351
+
352
+ io.tool_output("Completing authentication...")
353
+ analytics.event("oauth_flow_code_received", provider="openrouter")
354
+
355
+ # Exchange code for key
356
+ api_key = exchange_code_for_key(auth_code, code_verifier, io)
357
+
358
+ if api_key:
359
+ # Set env var for the current session immediately
360
+ os.environ["OPENROUTER_API_KEY"] = api_key
361
+
362
+ # Save the key to the oauth-keys.env file
363
+ try:
364
+ config_dir = os.path.expanduser("~/.aider")
365
+ os.makedirs(config_dir, exist_ok=True)
366
+ key_file = os.path.join(config_dir, "oauth-keys.env")
367
+ with open(key_file, "a", encoding="utf-8") as f:
368
+ f.write(f'OPENROUTER_API_KEY="{api_key}"\n')
369
+
370
+ io.tool_warning("Aider will load the OpenRouter key automatically in future sessions.")
371
+ io.tool_output()
372
+
373
+ analytics.event("oauth_flow_success", provider="openrouter")
374
+ return api_key
375
+ except Exception as e:
376
+ io.tool_error(f"Successfully obtained key, but failed to save it to file: {e}")
377
+ io.tool_warning("Set OPENROUTER_API_KEY environment variable for this session only.")
378
+ # Still return the key for the current session even if saving failed
379
+ analytics.event("oauth_flow_save_failed", provider="openrouter", reason=str(e))
380
+ return api_key
381
+ else:
382
+ io.tool_error("Authentication with OpenRouter failed.")
383
+ analytics.event("oauth_flow_failed", provider="openrouter", reason="code_exchange_failed")
384
+ return None
385
+
386
+
387
+ # Dummy Analytics class for testing
388
+ class DummyAnalytics:
389
+ def event(self, *args, **kwargs):
390
+ # print(f"Analytics Event: {args} {kwargs}") # Optional: print events
391
+ pass
392
+
393
+
394
+ def main():
395
+ """Main function to test the OpenRouter OAuth flow."""
396
+ print("Starting OpenRouter OAuth flow test...")
397
+
398
+ # Use a real IO object for interaction
399
+ io = InputOutput(
400
+ pretty=True,
401
+ yes=False,
402
+ input_history_file=None,
403
+ chat_history_file=None,
404
+ tool_output_color="BLUE",
405
+ tool_error_color="RED",
406
+ )
407
+ # Use a dummy analytics object
408
+ analytics = DummyAnalytics()
409
+
410
+ # Ensure OPENROUTER_API_KEY is not set, to trigger the flow naturally
411
+ # (though start_openrouter_oauth_flow doesn't check this itself)
412
+ if "OPENROUTER_API_KEY" in os.environ:
413
+ print("Warning: OPENROUTER_API_KEY is already set in environment.")
414
+ # del os.environ["OPENROUTER_API_KEY"] # Optionally unset it for testing
415
+
416
+ api_key = start_openrouter_oauth_flow(io, analytics)
417
+
418
+ if api_key:
419
+ print("\nOAuth flow completed successfully!")
420
+ print(f"Obtained API Key (first 5 chars): {api_key[:5]}...")
421
+ # Be careful printing the key, even partially
422
+ else:
423
+ print("\nOAuth flow failed or was cancelled.")
424
+
425
+ print("\nOpenRouter OAuth flow test finished.")
426
+
427
+
428
+ if __name__ == "__main__":
429
+ main()
aider/openrouter.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ OpenRouter model metadata caching and lookup.
3
+
4
+ This module keeps a local cached copy of the OpenRouter model list
5
+ (downloaded from ``https://openrouter.ai/api/v1/models``) and exposes a
6
+ helper class that returns metadata for a given model in a format compatible
7
+ with litellm’s ``get_model_info``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Dict
16
+
17
+ import requests
18
+
19
+
20
+ def _cost_per_token(val: str | None) -> float | None:
21
+ """Convert a price string (USD per token) to a float."""
22
+ if val in (None, "", "0"):
23
+ return 0.0 if val == "0" else None
24
+ try:
25
+ return float(val)
26
+ except Exception: # noqa: BLE001
27
+ return None
28
+
29
+
30
+ class OpenRouterModelManager:
31
+ MODELS_URL = "https://openrouter.ai/api/v1/models"
32
+ CACHE_TTL = 60 * 60 * 24 # 24 h
33
+
34
+ def __init__(self) -> None:
35
+ self.cache_dir = Path.home() / ".aider" / "caches"
36
+ self.cache_file = self.cache_dir / "openrouter_models.json"
37
+ self.content: Dict | None = None
38
+ self.verify_ssl: bool = True
39
+ self._cache_loaded = False
40
+
41
+ # ------------------------------------------------------------------ #
42
+ # Public API #
43
+ # ------------------------------------------------------------------ #
44
+ def set_verify_ssl(self, verify_ssl: bool) -> None:
45
+ """Enable/disable SSL verification for API requests."""
46
+ self.verify_ssl = verify_ssl
47
+
48
+ def get_model_info(self, model: str) -> Dict:
49
+ """
50
+ Return metadata for *model* or an empty ``dict`` when unknown.
51
+
52
+ ``model`` should use the aider naming convention, e.g.
53
+ ``openrouter/nousresearch/deephermes-3-mistral-24b-preview:free``.
54
+ """
55
+ self._ensure_content()
56
+ if not self.content or "data" not in self.content:
57
+ return {}
58
+
59
+ route = self._strip_prefix(model)
60
+
61
+ # Consider both the exact id and id without any “:suffix”.
62
+ candidates = {route}
63
+ if ":" in route:
64
+ candidates.add(route.split(":", 1)[0])
65
+
66
+ record = next((item for item in self.content["data"] if item.get("id") in candidates), None)
67
+ if not record:
68
+ return {}
69
+
70
+ context_len = (
71
+ record.get("top_provider", {}).get("context_length")
72
+ or record.get("context_length")
73
+ or None
74
+ )
75
+
76
+ pricing = record.get("pricing", {})
77
+ return {
78
+ "max_input_tokens": context_len,
79
+ "max_tokens": context_len,
80
+ "max_output_tokens": context_len,
81
+ "input_cost_per_token": _cost_per_token(pricing.get("prompt")),
82
+ "output_cost_per_token": _cost_per_token(pricing.get("completion")),
83
+ "litellm_provider": "openrouter",
84
+ }
85
+
86
+ # ------------------------------------------------------------------ #
87
+ # Internal helpers #
88
+ # ------------------------------------------------------------------ #
89
+ def _strip_prefix(self, model: str) -> str:
90
+ return model[len("openrouter/") :] if model.startswith("openrouter/") else model
91
+
92
+ def _ensure_content(self) -> None:
93
+ self._load_cache()
94
+ if not self.content:
95
+ self._update_cache()
96
+
97
+ def _load_cache(self) -> None:
98
+ if self._cache_loaded:
99
+ return
100
+ try:
101
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
102
+ if self.cache_file.exists():
103
+ cache_age = time.time() - self.cache_file.stat().st_mtime
104
+ if cache_age < self.CACHE_TTL:
105
+ try:
106
+ self.content = json.loads(self.cache_file.read_text())
107
+ except json.JSONDecodeError:
108
+ self.content = None
109
+ except OSError:
110
+ # Cache directory might be unwritable; ignore.
111
+ pass
112
+
113
+ self._cache_loaded = True
114
+
115
+ def _update_cache(self) -> None:
116
+ try:
117
+ response = requests.get(self.MODELS_URL, timeout=10, verify=self.verify_ssl)
118
+ if response.status_code == 200:
119
+ self.content = response.json()
120
+ try:
121
+ self.cache_file.write_text(json.dumps(self.content, indent=2))
122
+ except OSError:
123
+ pass # Non-fatal if we can’t write the cache
124
+ except Exception as ex: # noqa: BLE001
125
+ print(f"Failed to fetch OpenRouter model list: {ex}")
126
+ try:
127
+ self.cache_file.write_text("{}")
128
+ except OSError:
129
+ pass
aider/prompts.py ADDED
@@ -0,0 +1,56 @@
1
+ # flake8: noqa: E501
2
+
3
+
4
+ # COMMIT
5
+
6
+ # Conventional Commits text adapted from:
7
+ # https://www.conventionalcommits.org/en/v1.0.0/#summary
8
+ commit_system = """You are an expert software engineer that generates concise, \
9
+ one-line Git commit messages based on the provided diffs.
10
+ Review the provided context and diffs which are about to be committed to a git repo.
11
+ Review the diffs carefully.
12
+ Generate a one-line commit message for those changes.
13
+ The commit message should be structured as follows: <type>: <description>
14
+ Use these for <type>: fix, feat, build, chore, ci, docs, style, refactor, perf, test
15
+
16
+ Ensure the commit message:{language_instruction}
17
+ - Starts with the appropriate prefix.
18
+ - Is in the imperative mood (e.g., \"add feature\" not \"added feature\" or \"adding feature\").
19
+ - Does not exceed 72 characters.
20
+
21
+ Reply only with the one-line commit message, without any additional text, explanations, or line breaks.
22
+ """
23
+
24
+ # COMMANDS
25
+ undo_command_reply = (
26
+ "I did `git reset --hard HEAD~1` to discard the last edits. Please wait for further"
27
+ " instructions before attempting that change again. Feel free to ask relevant questions about"
28
+ " why the changes were reverted."
29
+ )
30
+
31
+ added_files = (
32
+ "I added these files to the chat: {fnames}\nLet me know if there are others we should add."
33
+ )
34
+
35
+
36
+ run_output = """I ran this command:
37
+
38
+ {command}
39
+
40
+ And got this output:
41
+
42
+ {output}
43
+ """
44
+
45
+ # CHAT HISTORY
46
+ summarize = """Summarize this conversation about programming from the user's perspective.
47
+ The user is 'I' and the AI assistant is 'you'.
48
+
49
+ The summary should be brief, focusing on the most recent messages.
50
+ Start a new paragraph when the topic changes.
51
+ Mention any function names, libraries, packages, and filenames that were discussed or edited.
52
+ Do not use markdown ```...``` fenced code blocks.
53
+ This is a partial conversation, so do not use concluding phrases like "Finally...".
54
+ """
55
+
56
+ summary_prefix = "This is a summary of our recent conversation:\n"
@@ -0,0 +1,7 @@
1
+ These scm files are all adapted from the github repositories listed here:
2
+
3
+ https://github.com/Goldziher/tree-sitter-language-pack/blob/main/sources/language_definitions.json
4
+
5
+ See this URL for information on the licenses of each repo:
6
+
7
+ https://github.com/Goldziher/tree-sitter-language-pack/
@@ -0,0 +1,5 @@
1
+ (function_declarator
2
+ declarator: (identifier) @name.definition.function) @definition.function
3
+
4
+ (call_expression
5
+ function: (identifier) @name.reference.call) @reference.call
@@ -0,0 +1,9 @@
1
+ (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
2
+
3
+ (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class
4
+
5
+ (function_declarator declarator: (identifier) @name.definition.function) @definition.function
6
+
7
+ (type_definition declarator: (type_identifier) @name.definition.type) @definition.type
8
+
9
+ (enum_specifier name: (type_identifier) @name.definition.type) @definition.type
@@ -0,0 +1,16 @@
1
+ ; Definitions
2
+ (intent_def
3
+ (intent) @name.definition.intent) @definition.intent
4
+
5
+ (slot_def
6
+ (slot) @name.definition.slot) @definition.slot
7
+
8
+ (alias_def
9
+ (alias) @name.definition.alias) @definition.alias
10
+
11
+ ; References
12
+ (slot_ref
13
+ (slot) @name.reference.slot) @reference.slot
14
+
15
+ (alias_ref
16
+ (alias) @name.reference.alias) @reference.alias
@@ -0,0 +1,7 @@
1
+ (list_lit
2
+ meta: _*
3
+ . (sym_lit name: (sym_name) @ignore)
4
+ . (sym_lit name: (sym_name) @name.definition.method)
5
+ (#match? @ignore "^def.*"))
6
+
7
+ (sym_lit name: (sym_name) @name.reference.call)