aider-ce 0.87.2.dev9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aider-ce might be problematic. Click here for more details.

Files changed (264) 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 +1014 -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/architect_coder.py +48 -0
  10. aider/coders/architect_prompts.py +40 -0
  11. aider/coders/ask_coder.py +9 -0
  12. aider/coders/ask_prompts.py +35 -0
  13. aider/coders/base_coder.py +3013 -0
  14. aider/coders/base_prompts.py +87 -0
  15. aider/coders/chat_chunks.py +64 -0
  16. aider/coders/context_coder.py +53 -0
  17. aider/coders/context_prompts.py +75 -0
  18. aider/coders/editblock_coder.py +657 -0
  19. aider/coders/editblock_fenced_coder.py +10 -0
  20. aider/coders/editblock_fenced_prompts.py +143 -0
  21. aider/coders/editblock_func_coder.py +141 -0
  22. aider/coders/editblock_func_prompts.py +27 -0
  23. aider/coders/editblock_prompts.py +177 -0
  24. aider/coders/editor_diff_fenced_coder.py +9 -0
  25. aider/coders/editor_diff_fenced_prompts.py +11 -0
  26. aider/coders/editor_editblock_coder.py +9 -0
  27. aider/coders/editor_editblock_prompts.py +21 -0
  28. aider/coders/editor_whole_coder.py +9 -0
  29. aider/coders/editor_whole_prompts.py +12 -0
  30. aider/coders/help_coder.py +16 -0
  31. aider/coders/help_prompts.py +46 -0
  32. aider/coders/navigator_coder.py +2711 -0
  33. aider/coders/navigator_legacy_prompts.py +338 -0
  34. aider/coders/navigator_prompts.py +530 -0
  35. aider/coders/patch_coder.py +706 -0
  36. aider/coders/patch_prompts.py +161 -0
  37. aider/coders/search_replace.py +757 -0
  38. aider/coders/shell.py +37 -0
  39. aider/coders/single_wholefile_func_coder.py +102 -0
  40. aider/coders/single_wholefile_func_prompts.py +27 -0
  41. aider/coders/udiff_coder.py +429 -0
  42. aider/coders/udiff_prompts.py +117 -0
  43. aider/coders/udiff_simple.py +14 -0
  44. aider/coders/udiff_simple_prompts.py +25 -0
  45. aider/coders/wholefile_coder.py +144 -0
  46. aider/coders/wholefile_func_coder.py +134 -0
  47. aider/coders/wholefile_func_prompts.py +27 -0
  48. aider/coders/wholefile_prompts.py +70 -0
  49. aider/commands.py +1946 -0
  50. aider/copypaste.py +72 -0
  51. aider/deprecated.py +126 -0
  52. aider/diffs.py +128 -0
  53. aider/dump.py +29 -0
  54. aider/editor.py +147 -0
  55. aider/exceptions.py +107 -0
  56. aider/format_settings.py +26 -0
  57. aider/gui.py +545 -0
  58. aider/help.py +163 -0
  59. aider/help_pats.py +19 -0
  60. aider/history.py +178 -0
  61. aider/io.py +1257 -0
  62. aider/linter.py +304 -0
  63. aider/llm.py +47 -0
  64. aider/main.py +1297 -0
  65. aider/mcp/__init__.py +94 -0
  66. aider/mcp/server.py +119 -0
  67. aider/mdstream.py +243 -0
  68. aider/models.py +1344 -0
  69. aider/onboarding.py +428 -0
  70. aider/openrouter.py +129 -0
  71. aider/prompts.py +56 -0
  72. aider/queries/tree-sitter-language-pack/README.md +7 -0
  73. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  74. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  75. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  76. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  77. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  91. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  92. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  93. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  94. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  95. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  96. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  97. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  98. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  99. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  100. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  101. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  102. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  103. aider/queries/tree-sitter-languages/README.md +23 -0
  104. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  105. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  106. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  107. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  108. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  109. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  110. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  111. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  112. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  113. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  114. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  115. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  116. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  117. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  118. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  119. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  120. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  121. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  122. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  123. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  124. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  125. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  126. aider/reasoning_tags.py +82 -0
  127. aider/repo.py +621 -0
  128. aider/repomap.py +988 -0
  129. aider/report.py +200 -0
  130. aider/resources/__init__.py +3 -0
  131. aider/resources/model-metadata.json +699 -0
  132. aider/resources/model-settings.yml +2046 -0
  133. aider/run_cmd.py +132 -0
  134. aider/scrape.py +284 -0
  135. aider/sendchat.py +61 -0
  136. aider/special.py +203 -0
  137. aider/tools/__init__.py +26 -0
  138. aider/tools/command.py +58 -0
  139. aider/tools/command_interactive.py +53 -0
  140. aider/tools/delete_block.py +120 -0
  141. aider/tools/delete_line.py +112 -0
  142. aider/tools/delete_lines.py +137 -0
  143. aider/tools/extract_lines.py +276 -0
  144. aider/tools/grep.py +171 -0
  145. aider/tools/indent_lines.py +155 -0
  146. aider/tools/insert_block.py +211 -0
  147. aider/tools/list_changes.py +51 -0
  148. aider/tools/ls.py +49 -0
  149. aider/tools/make_editable.py +46 -0
  150. aider/tools/make_readonly.py +29 -0
  151. aider/tools/remove.py +48 -0
  152. aider/tools/replace_all.py +77 -0
  153. aider/tools/replace_line.py +125 -0
  154. aider/tools/replace_lines.py +160 -0
  155. aider/tools/replace_text.py +125 -0
  156. aider/tools/show_numbered_context.py +101 -0
  157. aider/tools/tool_utils.py +313 -0
  158. aider/tools/undo_change.py +60 -0
  159. aider/tools/view.py +13 -0
  160. aider/tools/view_files_at_glob.py +65 -0
  161. aider/tools/view_files_matching.py +103 -0
  162. aider/tools/view_files_with_symbol.py +121 -0
  163. aider/urls.py +17 -0
  164. aider/utils.py +454 -0
  165. aider/versioncheck.py +113 -0
  166. aider/voice.py +187 -0
  167. aider/waiting.py +221 -0
  168. aider/watch.py +318 -0
  169. aider/watch_prompts.py +12 -0
  170. aider/website/Gemfile +8 -0
  171. aider/website/_includes/blame.md +162 -0
  172. aider/website/_includes/get-started.md +22 -0
  173. aider/website/_includes/help-tip.md +5 -0
  174. aider/website/_includes/help.md +24 -0
  175. aider/website/_includes/install.md +5 -0
  176. aider/website/_includes/keys.md +4 -0
  177. aider/website/_includes/model-warnings.md +67 -0
  178. aider/website/_includes/multi-line.md +22 -0
  179. aider/website/_includes/python-m-aider.md +5 -0
  180. aider/website/_includes/recording.css +228 -0
  181. aider/website/_includes/recording.md +34 -0
  182. aider/website/_includes/replit-pipx.md +9 -0
  183. aider/website/_includes/works-best.md +1 -0
  184. aider/website/_sass/custom/custom.scss +103 -0
  185. aider/website/docs/config/adv-model-settings.md +2260 -0
  186. aider/website/docs/config/aider_conf.md +548 -0
  187. aider/website/docs/config/api-keys.md +90 -0
  188. aider/website/docs/config/dotenv.md +493 -0
  189. aider/website/docs/config/editor.md +127 -0
  190. aider/website/docs/config/mcp.md +95 -0
  191. aider/website/docs/config/model-aliases.md +104 -0
  192. aider/website/docs/config/options.md +890 -0
  193. aider/website/docs/config/reasoning.md +210 -0
  194. aider/website/docs/config.md +44 -0
  195. aider/website/docs/faq.md +384 -0
  196. aider/website/docs/git.md +76 -0
  197. aider/website/docs/index.md +47 -0
  198. aider/website/docs/install/codespaces.md +39 -0
  199. aider/website/docs/install/docker.md +57 -0
  200. aider/website/docs/install/optional.md +100 -0
  201. aider/website/docs/install/replit.md +8 -0
  202. aider/website/docs/install.md +115 -0
  203. aider/website/docs/languages.md +264 -0
  204. aider/website/docs/legal/contributor-agreement.md +111 -0
  205. aider/website/docs/legal/privacy.md +104 -0
  206. aider/website/docs/llms/anthropic.md +77 -0
  207. aider/website/docs/llms/azure.md +48 -0
  208. aider/website/docs/llms/bedrock.md +132 -0
  209. aider/website/docs/llms/cohere.md +34 -0
  210. aider/website/docs/llms/deepseek.md +32 -0
  211. aider/website/docs/llms/gemini.md +49 -0
  212. aider/website/docs/llms/github.md +111 -0
  213. aider/website/docs/llms/groq.md +36 -0
  214. aider/website/docs/llms/lm-studio.md +39 -0
  215. aider/website/docs/llms/ollama.md +75 -0
  216. aider/website/docs/llms/openai-compat.md +39 -0
  217. aider/website/docs/llms/openai.md +58 -0
  218. aider/website/docs/llms/openrouter.md +78 -0
  219. aider/website/docs/llms/other.md +111 -0
  220. aider/website/docs/llms/vertex.md +50 -0
  221. aider/website/docs/llms/warnings.md +10 -0
  222. aider/website/docs/llms/xai.md +53 -0
  223. aider/website/docs/llms.md +54 -0
  224. aider/website/docs/more/analytics.md +127 -0
  225. aider/website/docs/more/edit-formats.md +116 -0
  226. aider/website/docs/more/infinite-output.md +159 -0
  227. aider/website/docs/more-info.md +8 -0
  228. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  229. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  230. aider/website/docs/recordings/index.md +21 -0
  231. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  232. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  233. aider/website/docs/repomap.md +112 -0
  234. aider/website/docs/scripting.md +100 -0
  235. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  236. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  237. aider/website/docs/troubleshooting/imports.md +62 -0
  238. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  239. aider/website/docs/troubleshooting/support.md +79 -0
  240. aider/website/docs/troubleshooting/token-limits.md +96 -0
  241. aider/website/docs/troubleshooting/warnings.md +12 -0
  242. aider/website/docs/troubleshooting.md +11 -0
  243. aider/website/docs/usage/browser.md +57 -0
  244. aider/website/docs/usage/caching.md +49 -0
  245. aider/website/docs/usage/commands.md +133 -0
  246. aider/website/docs/usage/conventions.md +119 -0
  247. aider/website/docs/usage/copypaste.md +121 -0
  248. aider/website/docs/usage/images-urls.md +48 -0
  249. aider/website/docs/usage/lint-test.md +118 -0
  250. aider/website/docs/usage/modes.md +211 -0
  251. aider/website/docs/usage/not-code.md +179 -0
  252. aider/website/docs/usage/notifications.md +87 -0
  253. aider/website/docs/usage/tips.md +79 -0
  254. aider/website/docs/usage/tutorials.md +30 -0
  255. aider/website/docs/usage/voice.md +121 -0
  256. aider/website/docs/usage/watch.md +294 -0
  257. aider/website/docs/usage.md +102 -0
  258. aider/website/share/index.md +101 -0
  259. aider_ce-0.87.2.dev9.dist-info/METADATA +543 -0
  260. aider_ce-0.87.2.dev9.dist-info/RECORD +264 -0
  261. aider_ce-0.87.2.dev9.dist-info/WHEEL +5 -0
  262. aider_ce-0.87.2.dev9.dist-info/entry_points.txt +3 -0
  263. aider_ce-0.87.2.dev9.dist-info/licenses/LICENSE.txt +202 -0
  264. aider_ce-0.87.2.dev9.dist-info/top_level.txt +1 -0
aider/models.py ADDED
@@ -0,0 +1,1344 @@
1
+ import difflib
2
+ import hashlib
3
+ import importlib.resources
4
+ import json
5
+ import math
6
+ import os
7
+ import platform
8
+ import sys
9
+ import time
10
+ from dataclasses import dataclass, fields
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Optional, Union
14
+
15
+ import json5
16
+ import yaml
17
+ from PIL import Image
18
+
19
+ from aider import __version__
20
+ from aider.dump import dump # noqa: F401
21
+ from aider.llm import litellm
22
+ from aider.openrouter import OpenRouterModelManager
23
+ from aider.sendchat import ensure_alternating_roles, sanity_check_messages
24
+ from aider.utils import check_pip_install_extra
25
+
26
+ RETRY_TIMEOUT = 60
27
+
28
+ request_timeout = 600
29
+
30
+ DEFAULT_MODEL_NAME = "gpt-4o"
31
+ ANTHROPIC_BETA_HEADER = "prompt-caching-2024-07-31,pdfs-2024-09-25"
32
+
33
+ OPENAI_MODELS = """
34
+ o1
35
+ o1-preview
36
+ o1-mini
37
+ o3-mini
38
+ gpt-4
39
+ gpt-4o
40
+ gpt-4o-2024-05-13
41
+ gpt-4-turbo-preview
42
+ gpt-4-0314
43
+ gpt-4-0613
44
+ gpt-4-32k
45
+ gpt-4-32k-0314
46
+ gpt-4-32k-0613
47
+ gpt-4-turbo
48
+ gpt-4-turbo-2024-04-09
49
+ gpt-4-1106-preview
50
+ gpt-4-0125-preview
51
+ gpt-4-vision-preview
52
+ gpt-4-1106-vision-preview
53
+ gpt-4o-mini
54
+ gpt-4o-mini-2024-07-18
55
+ gpt-3.5-turbo
56
+ gpt-3.5-turbo-0301
57
+ gpt-3.5-turbo-0613
58
+ gpt-3.5-turbo-1106
59
+ gpt-3.5-turbo-0125
60
+ gpt-3.5-turbo-16k
61
+ gpt-3.5-turbo-16k-0613
62
+ """
63
+
64
+ OPENAI_MODELS = [ln.strip() for ln in OPENAI_MODELS.splitlines() if ln.strip()]
65
+
66
+ ANTHROPIC_MODELS = """
67
+ claude-2
68
+ claude-2.1
69
+ claude-3-haiku-20240307
70
+ claude-3-5-haiku-20241022
71
+ claude-3-opus-20240229
72
+ claude-3-sonnet-20240229
73
+ claude-3-5-sonnet-20240620
74
+ claude-3-5-sonnet-20241022
75
+ claude-sonnet-4-20250514
76
+ claude-opus-4-20250514
77
+ """
78
+
79
+ ANTHROPIC_MODELS = [ln.strip() for ln in ANTHROPIC_MODELS.splitlines() if ln.strip()]
80
+
81
+ # Mapping of model aliases to their canonical names
82
+ MODEL_ALIASES = {
83
+ # Claude models
84
+ "sonnet": "anthropic/claude-sonnet-4-20250514",
85
+ "haiku": "claude-3-5-haiku-20241022",
86
+ "opus": "claude-opus-4-20250514",
87
+ # GPT models
88
+ "4": "gpt-4-0613",
89
+ "4o": "gpt-4o",
90
+ "4-turbo": "gpt-4-1106-preview",
91
+ "35turbo": "gpt-3.5-turbo",
92
+ "35-turbo": "gpt-3.5-turbo",
93
+ "3": "gpt-3.5-turbo",
94
+ # Other models
95
+ "deepseek": "deepseek/deepseek-chat",
96
+ "flash": "gemini/gemini-2.5-flash",
97
+ "flash-lite": "gemini/gemini-2.5-flash-lite",
98
+ "quasar": "openrouter/openrouter/quasar-alpha",
99
+ "r1": "deepseek/deepseek-reasoner",
100
+ "gemini-2.5-pro": "gemini/gemini-2.5-pro",
101
+ "gemini": "gemini/gemini-2.5-pro",
102
+ "gemini-exp": "gemini/gemini-2.5-pro-exp-03-25",
103
+ "grok3": "xai/grok-3-beta",
104
+ "optimus": "openrouter/openrouter/optimus-alpha",
105
+ }
106
+ # Model metadata loaded from resources and user's files.
107
+
108
+
109
+ @dataclass
110
+ class ModelSettings:
111
+ # Model class needs to have each of these as well
112
+ name: str
113
+ edit_format: str = "whole"
114
+ weak_model_name: Optional[str] = None
115
+ use_repo_map: bool = False
116
+ send_undo_reply: bool = False
117
+ lazy: bool = False
118
+ overeager: bool = False
119
+ reminder: str = "user"
120
+ examples_as_sys_msg: bool = False
121
+ extra_params: Optional[dict] = None
122
+ cache_control: bool = False
123
+ caches_by_default: bool = False
124
+ use_system_prompt: bool = True
125
+ use_temperature: Union[bool, float] = True
126
+ streaming: bool = True
127
+ editor_model_name: Optional[str] = None
128
+ editor_edit_format: Optional[str] = None
129
+ reasoning_tag: Optional[str] = None
130
+ remove_reasoning: Optional[str] = None # Deprecated alias for reasoning_tag
131
+ system_prompt_prefix: Optional[str] = None
132
+ accepts_settings: Optional[list] = None
133
+
134
+
135
+ # Load model settings from package resource
136
+ MODEL_SETTINGS = []
137
+ with importlib.resources.open_text("aider.resources", "model-settings.yml") as f:
138
+ model_settings_list = yaml.safe_load(f)
139
+ for model_settings_dict in model_settings_list:
140
+ MODEL_SETTINGS.append(ModelSettings(**model_settings_dict))
141
+
142
+
143
+ class ModelInfoManager:
144
+ MODEL_INFO_URL = (
145
+ "https://raw.githubusercontent.com/BerriAI/litellm/main/"
146
+ "model_prices_and_context_window.json"
147
+ )
148
+ CACHE_TTL = 60 * 60 * 24 # 24 hours
149
+
150
+ def __init__(self):
151
+ self.cache_dir = Path.home() / ".aider" / "caches"
152
+ self.cache_file = self.cache_dir / "model_prices_and_context_window.json"
153
+ self.content = None
154
+ self.local_model_metadata = {}
155
+ self.verify_ssl = True
156
+ self._cache_loaded = False
157
+
158
+ # Manager for the cached OpenRouter model database
159
+ self.openrouter_manager = OpenRouterModelManager()
160
+
161
+ def set_verify_ssl(self, verify_ssl):
162
+ self.verify_ssl = verify_ssl
163
+ if hasattr(self, "openrouter_manager"):
164
+ self.openrouter_manager.set_verify_ssl(verify_ssl)
165
+
166
+ def _load_cache(self):
167
+ if self._cache_loaded:
168
+ return
169
+
170
+ try:
171
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
172
+ if self.cache_file.exists():
173
+ cache_age = time.time() - self.cache_file.stat().st_mtime
174
+ if cache_age < self.CACHE_TTL:
175
+ try:
176
+ self.content = json.loads(self.cache_file.read_text())
177
+ except json.JSONDecodeError:
178
+ # If the cache file is corrupted, treat it as missing
179
+ self.content = None
180
+ except OSError:
181
+ pass
182
+
183
+ self._cache_loaded = True
184
+
185
+ def _update_cache(self):
186
+ try:
187
+ import requests
188
+
189
+ # Respect the --no-verify-ssl switch
190
+ response = requests.get(self.MODEL_INFO_URL, timeout=5, verify=self.verify_ssl)
191
+ if response.status_code == 200:
192
+ self.content = response.json()
193
+ try:
194
+ self.cache_file.write_text(json.dumps(self.content, indent=4))
195
+ except OSError:
196
+ pass
197
+ except Exception as ex:
198
+ print(str(ex))
199
+ try:
200
+ # Save empty dict to cache file on failure
201
+ self.cache_file.write_text("{}")
202
+ except OSError:
203
+ pass
204
+
205
+ def get_model_from_cached_json_db(self, model):
206
+ data = self.local_model_metadata.get(model)
207
+ if data:
208
+ return data
209
+
210
+ # Ensure cache is loaded before checking content
211
+ self._load_cache()
212
+
213
+ if not self.content:
214
+ self._update_cache()
215
+
216
+ if not self.content:
217
+ return dict()
218
+
219
+ info = self.content.get(model, dict())
220
+ if info:
221
+ return info
222
+
223
+ pieces = model.split("/")
224
+ if len(pieces) == 2:
225
+ info = self.content.get(pieces[1])
226
+ if info and info.get("litellm_provider") == pieces[0]:
227
+ return info
228
+
229
+ return dict()
230
+
231
+ def get_model_info(self, model):
232
+ cached_info = self.get_model_from_cached_json_db(model)
233
+
234
+ litellm_info = None
235
+ if litellm._lazy_module or not cached_info:
236
+ try:
237
+ litellm_info = litellm.get_model_info(model)
238
+ except Exception as ex:
239
+ if "model_prices_and_context_window.json" not in str(ex):
240
+ print(str(ex))
241
+
242
+ if litellm_info:
243
+ return litellm_info
244
+
245
+ if not cached_info and model.startswith("openrouter/"):
246
+ # First try using the locally cached OpenRouter model database
247
+ openrouter_info = self.openrouter_manager.get_model_info(model)
248
+ if openrouter_info:
249
+ return openrouter_info
250
+
251
+ # Fallback to legacy web-scraping if the API cache does not contain the model
252
+ openrouter_info = self.fetch_openrouter_model_info(model)
253
+ if openrouter_info:
254
+ return openrouter_info
255
+
256
+ return cached_info
257
+
258
+ def fetch_openrouter_model_info(self, model):
259
+ """
260
+ Fetch model info by scraping the openrouter model page.
261
+ Expected URL: https://openrouter.ai/<model_route>
262
+ Example: openrouter/qwen/qwen-2.5-72b-instruct:free
263
+ Returns a dict with keys: max_tokens, max_input_tokens, max_output_tokens,
264
+ input_cost_per_token, output_cost_per_token.
265
+ """
266
+ url_part = model[len("openrouter/") :]
267
+ url = "https://openrouter.ai/" + url_part
268
+ try:
269
+ import requests
270
+
271
+ response = requests.get(url, timeout=5, verify=self.verify_ssl)
272
+ if response.status_code != 200:
273
+ return {}
274
+ html = response.text
275
+ import re
276
+
277
+ if re.search(
278
+ rf"The model\s*.*{re.escape(url_part)}.* is not available", html, re.IGNORECASE
279
+ ):
280
+ print(f"\033[91mError: Model '{url_part}' is not available\033[0m")
281
+ return {}
282
+ text = re.sub(r"<[^>]+>", " ", html)
283
+ context_match = re.search(r"([\d,]+)\s*context", text)
284
+ if context_match:
285
+ context_str = context_match.group(1).replace(",", "")
286
+ context_size = int(context_str)
287
+ else:
288
+ context_size = None
289
+ input_cost_match = re.search(r"\$\s*([\d.]+)\s*/M input tokens", text, re.IGNORECASE)
290
+ output_cost_match = re.search(r"\$\s*([\d.]+)\s*/M output tokens", text, re.IGNORECASE)
291
+ input_cost = float(input_cost_match.group(1)) / 1000000 if input_cost_match else None
292
+ output_cost = float(output_cost_match.group(1)) / 1000000 if output_cost_match else None
293
+ if context_size is None or input_cost is None or output_cost is None:
294
+ return {}
295
+ params = {
296
+ "max_input_tokens": context_size,
297
+ "max_tokens": context_size,
298
+ "max_output_tokens": context_size,
299
+ "input_cost_per_token": input_cost,
300
+ "output_cost_per_token": output_cost,
301
+ }
302
+ return params
303
+ except Exception as e:
304
+ print("Error fetching openrouter info:", str(e))
305
+ return {}
306
+
307
+
308
+ model_info_manager = ModelInfoManager()
309
+
310
+
311
+ class Model(ModelSettings):
312
+ def __init__(
313
+ self, model, weak_model=None, editor_model=None, editor_edit_format=None, verbose=False
314
+ ):
315
+ # Map any alias to its canonical name
316
+ model = MODEL_ALIASES.get(model, model)
317
+
318
+ self.name = model
319
+ self.verbose = verbose
320
+
321
+ self.max_chat_history_tokens = 1024
322
+ self.weak_model = None
323
+ self.editor_model = None
324
+
325
+ # Find the extra settings
326
+ self.extra_model_settings = next(
327
+ (ms for ms in MODEL_SETTINGS if ms.name == "aider/extra_params"), None
328
+ )
329
+
330
+ self.info = self.get_model_info(model)
331
+
332
+ # Are all needed keys/params available?
333
+ res = self.validate_environment()
334
+ self.missing_keys = res.get("missing_keys")
335
+ self.keys_in_environment = res.get("keys_in_environment")
336
+
337
+ max_input_tokens = self.info.get("max_input_tokens") or 0
338
+ # Calculate max_chat_history_tokens as 1/16th of max_input_tokens,
339
+ # with minimum 1k and maximum 8k
340
+ self.max_chat_history_tokens = min(max(max_input_tokens / 16, 1024), 8192)
341
+
342
+ self.configure_model_settings(model)
343
+ if weak_model is False:
344
+ self.weak_model_name = None
345
+ else:
346
+ self.get_weak_model(weak_model)
347
+
348
+ if editor_model is False:
349
+ self.editor_model_name = None
350
+ else:
351
+ self.get_editor_model(editor_model, editor_edit_format)
352
+
353
+ def get_model_info(self, model):
354
+ return model_info_manager.get_model_info(model)
355
+
356
+ def _copy_fields(self, source):
357
+ """Helper to copy fields from a ModelSettings instance to self"""
358
+ for field in fields(ModelSettings):
359
+ val = getattr(source, field.name)
360
+ setattr(self, field.name, val)
361
+
362
+ # Handle backward compatibility: if remove_reasoning is set but reasoning_tag isn't,
363
+ # use remove_reasoning's value for reasoning_tag
364
+ if self.reasoning_tag is None and self.remove_reasoning is not None:
365
+ self.reasoning_tag = self.remove_reasoning
366
+
367
+ def configure_model_settings(self, model):
368
+ # Look for exact model match
369
+ exact_match = False
370
+ for ms in MODEL_SETTINGS:
371
+ # direct match, or match "provider/<model>"
372
+ if model == ms.name:
373
+ self._copy_fields(ms)
374
+ exact_match = True
375
+ break # Continue to apply overrides
376
+
377
+ # Initialize accepts_settings if it's None
378
+ if self.accepts_settings is None:
379
+ self.accepts_settings = []
380
+
381
+ model = model.lower()
382
+
383
+ # If no exact match, try generic settings
384
+ if not exact_match:
385
+ self.apply_generic_model_settings(model)
386
+
387
+ # Apply override settings last if they exist
388
+ if (
389
+ self.extra_model_settings
390
+ and self.extra_model_settings.extra_params
391
+ and self.extra_model_settings.name == "aider/extra_params"
392
+ ):
393
+ # Initialize extra_params if it doesn't exist
394
+ if not self.extra_params:
395
+ self.extra_params = {}
396
+
397
+ # Deep merge the extra_params dicts
398
+ for key, value in self.extra_model_settings.extra_params.items():
399
+ if isinstance(value, dict) and isinstance(self.extra_params.get(key), dict):
400
+ # For nested dicts, merge recursively
401
+ self.extra_params[key] = {**self.extra_params[key], **value}
402
+ else:
403
+ # For non-dict values, simply update
404
+ self.extra_params[key] = value
405
+
406
+ # Ensure OpenRouter models accept thinking_tokens and reasoning_effort
407
+ if self.name.startswith("openrouter/"):
408
+ if self.accepts_settings is None:
409
+ self.accepts_settings = []
410
+ if "thinking_tokens" not in self.accepts_settings:
411
+ self.accepts_settings.append("thinking_tokens")
412
+ if "reasoning_effort" not in self.accepts_settings:
413
+ self.accepts_settings.append("reasoning_effort")
414
+
415
+ def apply_generic_model_settings(self, model):
416
+ if "/o3-mini" in model:
417
+ self.edit_format = "diff"
418
+ self.use_repo_map = True
419
+ self.use_temperature = False
420
+ self.system_prompt_prefix = "Formatting re-enabled. "
421
+ self.system_prompt_prefix = "Formatting re-enabled. "
422
+ if "reasoning_effort" not in self.accepts_settings:
423
+ self.accepts_settings.append("reasoning_effort")
424
+ return # <--
425
+
426
+ if "gpt-4.1-mini" in model:
427
+ self.edit_format = "diff"
428
+ self.use_repo_map = True
429
+ self.reminder = "sys"
430
+ self.examples_as_sys_msg = False
431
+ return # <--
432
+
433
+ if "gpt-4.1" in model:
434
+ self.edit_format = "diff"
435
+ self.use_repo_map = True
436
+ self.reminder = "sys"
437
+ self.examples_as_sys_msg = False
438
+ return # <--
439
+
440
+ last_segment = model.split("/")[-1]
441
+ if last_segment in ("gpt-5", "gpt-5-2025-08-07"):
442
+ self.use_temperature = False
443
+ self.edit_format = "diff"
444
+ if "reasoning_effort" not in self.accepts_settings:
445
+ self.accepts_settings.append("reasoning_effort")
446
+ return # <--
447
+
448
+ if "/o1-mini" in model:
449
+ self.use_repo_map = True
450
+ self.use_temperature = False
451
+ self.use_system_prompt = False
452
+ return # <--
453
+
454
+ if "/o1-preview" in model:
455
+ self.edit_format = "diff"
456
+ self.use_repo_map = True
457
+ self.use_temperature = False
458
+ self.use_system_prompt = False
459
+ return # <--
460
+
461
+ if "/o1" in model:
462
+ self.edit_format = "diff"
463
+ self.use_repo_map = True
464
+ self.use_temperature = False
465
+ self.streaming = False
466
+ self.system_prompt_prefix = "Formatting re-enabled. "
467
+ if "reasoning_effort" not in self.accepts_settings:
468
+ self.accepts_settings.append("reasoning_effort")
469
+ return # <--
470
+
471
+ if "deepseek" in model and "v3" in model:
472
+ self.edit_format = "diff"
473
+ self.use_repo_map = True
474
+ self.reminder = "sys"
475
+ self.examples_as_sys_msg = True
476
+ return # <--
477
+
478
+ if "deepseek" in model and ("r1" in model or "reasoning" in model):
479
+ self.edit_format = "diff"
480
+ self.use_repo_map = True
481
+ self.examples_as_sys_msg = True
482
+ self.use_temperature = False
483
+ self.reasoning_tag = "think"
484
+ return # <--
485
+
486
+ if ("llama3" in model or "llama-3" in model) and "70b" in model:
487
+ self.edit_format = "diff"
488
+ self.use_repo_map = True
489
+ self.send_undo_reply = True
490
+ self.examples_as_sys_msg = True
491
+ return # <--
492
+
493
+ if "gpt-4-turbo" in model or ("gpt-4-" in model and "-preview" in model):
494
+ self.edit_format = "udiff"
495
+ self.use_repo_map = True
496
+ self.send_undo_reply = True
497
+ return # <--
498
+
499
+ if "gpt-4" in model or "claude-3-opus" in model:
500
+ self.edit_format = "diff"
501
+ self.use_repo_map = True
502
+ self.send_undo_reply = True
503
+ return # <--
504
+
505
+ if "gpt-3.5" in model or "gpt-4" in model:
506
+ self.reminder = "sys"
507
+ return # <--
508
+
509
+ if "3-7-sonnet" in model:
510
+ self.edit_format = "diff"
511
+ self.use_repo_map = True
512
+ self.examples_as_sys_msg = True
513
+ self.reminder = "user"
514
+ if "thinking_tokens" not in self.accepts_settings:
515
+ self.accepts_settings.append("thinking_tokens")
516
+ return # <--
517
+
518
+ if "3.5-sonnet" in model or "3-5-sonnet" in model:
519
+ self.edit_format = "diff"
520
+ self.use_repo_map = True
521
+ self.examples_as_sys_msg = True
522
+ self.reminder = "user"
523
+ return # <--
524
+
525
+ if model.startswith("o1-") or "/o1-" in model:
526
+ self.use_system_prompt = False
527
+ self.use_temperature = False
528
+ return # <--
529
+
530
+ if (
531
+ "qwen" in model
532
+ and "coder" in model
533
+ and ("2.5" in model or "2-5" in model)
534
+ and "32b" in model
535
+ ):
536
+ self.edit_format = "diff"
537
+ self.editor_edit_format = "editor-diff"
538
+ self.use_repo_map = True
539
+ return # <--
540
+
541
+ if "qwq" in model and "32b" in model and "preview" not in model:
542
+ self.edit_format = "diff"
543
+ self.editor_edit_format = "editor-diff"
544
+ self.use_repo_map = True
545
+ self.reasoning_tag = "think"
546
+ self.examples_as_sys_msg = True
547
+ self.use_temperature = 0.6
548
+ self.extra_params = dict(top_p=0.95)
549
+ return # <--
550
+
551
+ if "qwen3" in model:
552
+ self.edit_format = "diff"
553
+ self.use_repo_map = True
554
+ if "235b" in model:
555
+ self.system_prompt_prefix = "/no_think"
556
+ self.use_temperature = 0.7
557
+ self.extra_params = {"top_p": 0.8, "top_k": 20, "min_p": 0.0}
558
+ else:
559
+ self.examples_as_sys_msg = True
560
+ self.use_temperature = 0.6
561
+ self.reasoning_tag = "think"
562
+ self.extra_params = {"top_p": 0.95, "top_k": 20, "min_p": 0.0}
563
+ return # <--
564
+
565
+ # use the defaults
566
+ if self.edit_format == "diff":
567
+ self.use_repo_map = True
568
+ return # <--
569
+
570
+ def __str__(self):
571
+ return self.name
572
+
573
+ def get_weak_model(self, provided_weak_model_name):
574
+ # If weak_model_name is provided, override the model settings
575
+ if provided_weak_model_name:
576
+ self.weak_model_name = provided_weak_model_name
577
+
578
+ if not self.weak_model_name:
579
+ self.weak_model = self
580
+ return
581
+
582
+ if self.weak_model_name == self.name:
583
+ self.weak_model = self
584
+ return
585
+
586
+ self.weak_model = Model(
587
+ self.weak_model_name,
588
+ weak_model=False,
589
+ )
590
+ return self.weak_model
591
+
592
+ def commit_message_models(self):
593
+ return [self.weak_model, self]
594
+
595
+ def get_editor_model(self, provided_editor_model_name, editor_edit_format):
596
+ # If editor_model_name is provided, override the model settings
597
+ if provided_editor_model_name:
598
+ self.editor_model_name = provided_editor_model_name
599
+ if editor_edit_format:
600
+ self.editor_edit_format = editor_edit_format
601
+
602
+ if not self.editor_model_name or self.editor_model_name == self.name:
603
+ self.editor_model = self
604
+ else:
605
+ self.editor_model = Model(
606
+ self.editor_model_name,
607
+ editor_model=False,
608
+ )
609
+
610
+ if not self.editor_edit_format:
611
+ self.editor_edit_format = self.editor_model.edit_format
612
+ if self.editor_edit_format in ("diff", "whole", "diff-fenced"):
613
+ self.editor_edit_format = "editor-" + self.editor_edit_format
614
+
615
+ return self.editor_model
616
+
617
+ def tokenizer(self, text):
618
+ return litellm.encode(model=self.name, text=text)
619
+
620
+ def token_count(self, messages):
621
+ if isinstance(messages, dict):
622
+ messages = [messages]
623
+
624
+ if isinstance(messages, list):
625
+ try:
626
+ return litellm.token_counter(model=self.name, messages=messages)
627
+ except Exception:
628
+ pass # fall back to raw tokenizer
629
+
630
+ if not self.tokenizer:
631
+ return 0
632
+
633
+ if isinstance(messages, str):
634
+ msgs = messages
635
+ else:
636
+ msgs = json.dumps(messages)
637
+
638
+ try:
639
+ return len(self.tokenizer(msgs))
640
+ except Exception as err:
641
+ print(f"Unable to count tokens with tokenizer: {err}")
642
+ return 0
643
+
644
+ def token_count_for_image(self, fname):
645
+ """
646
+ Calculate the token cost for an image assuming high detail.
647
+ The token cost is determined by the size of the image.
648
+ :param fname: The filename of the image.
649
+ :return: The token cost for the image.
650
+ """
651
+ width, height = self.get_image_size(fname)
652
+
653
+ # If the image is larger than 2048 in any dimension, scale it down to fit within 2048x2048
654
+ max_dimension = max(width, height)
655
+ if max_dimension > 2048:
656
+ scale_factor = 2048 / max_dimension
657
+ width = int(width * scale_factor)
658
+ height = int(height * scale_factor)
659
+
660
+ # Scale the image such that the shortest side is 768 pixels long
661
+ min_dimension = min(width, height)
662
+ scale_factor = 768 / min_dimension
663
+ width = int(width * scale_factor)
664
+ height = int(height * scale_factor)
665
+
666
+ # Calculate the number of 512x512 tiles needed to cover the image
667
+ tiles_width = math.ceil(width / 512)
668
+ tiles_height = math.ceil(height / 512)
669
+ num_tiles = tiles_width * tiles_height
670
+
671
+ # Each tile costs 170 tokens, and there's an additional fixed cost of 85 tokens
672
+ token_cost = num_tiles * 170 + 85
673
+ return token_cost
674
+
675
+ def get_image_size(self, fname):
676
+ """
677
+ Retrieve the size of an image.
678
+ :param fname: The filename of the image.
679
+ :return: A tuple (width, height) representing the image size in pixels.
680
+ """
681
+ with Image.open(fname) as img:
682
+ return img.size
683
+
684
+ def fast_validate_environment(self):
685
+ """Fast path for common models. Avoids forcing litellm import."""
686
+
687
+ model = self.name
688
+
689
+ pieces = model.split("/")
690
+ if len(pieces) > 1:
691
+ provider = pieces[0]
692
+ else:
693
+ provider = None
694
+
695
+ keymap = dict(
696
+ openrouter="OPENROUTER_API_KEY",
697
+ openai="OPENAI_API_KEY",
698
+ deepseek="DEEPSEEK_API_KEY",
699
+ gemini="GEMINI_API_KEY",
700
+ anthropic="ANTHROPIC_API_KEY",
701
+ groq="GROQ_API_KEY",
702
+ fireworks_ai="FIREWORKS_API_KEY",
703
+ )
704
+ var = None
705
+ if model in OPENAI_MODELS:
706
+ var = "OPENAI_API_KEY"
707
+ elif model in ANTHROPIC_MODELS:
708
+ var = "ANTHROPIC_API_KEY"
709
+ else:
710
+ var = keymap.get(provider)
711
+
712
+ if var and os.environ.get(var):
713
+ return dict(keys_in_environment=[var], missing_keys=[])
714
+
715
+ def validate_environment(self):
716
+ res = self.fast_validate_environment()
717
+ if res:
718
+ return res
719
+
720
+ # https://github.com/BerriAI/litellm/issues/3190
721
+
722
+ model = self.name
723
+ res = litellm.validate_environment(model)
724
+
725
+ # If missing AWS credential keys but AWS_PROFILE is set, consider AWS credentials valid
726
+ if res["missing_keys"] and any(
727
+ key in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] for key in res["missing_keys"]
728
+ ):
729
+ if model.startswith("bedrock/") or model.startswith("us.anthropic."):
730
+ if os.environ.get("AWS_PROFILE"):
731
+ res["missing_keys"] = [
732
+ k
733
+ for k in res["missing_keys"]
734
+ if k not in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
735
+ ]
736
+ if not res["missing_keys"]:
737
+ res["keys_in_environment"] = True
738
+
739
+ if res["keys_in_environment"]:
740
+ return res
741
+ if res["missing_keys"]:
742
+ return res
743
+
744
+ provider = self.info.get("litellm_provider", "").lower()
745
+ if provider == "cohere_chat":
746
+ return validate_variables(["COHERE_API_KEY"])
747
+ if provider == "gemini":
748
+ return validate_variables(["GEMINI_API_KEY"])
749
+ if provider == "groq":
750
+ return validate_variables(["GROQ_API_KEY"])
751
+
752
+ return res
753
+
754
+ def get_repo_map_tokens(self):
755
+ map_tokens = 1024
756
+ max_inp_tokens = self.info.get("max_input_tokens")
757
+ if max_inp_tokens:
758
+ map_tokens = max_inp_tokens / 8
759
+ map_tokens = min(map_tokens, 4096)
760
+ map_tokens = max(map_tokens, 1024)
761
+ return map_tokens
762
+
763
+ def set_reasoning_effort(self, effort):
764
+ """Set the reasoning effort parameter for models that support it"""
765
+ if effort is not None:
766
+ if self.name.startswith("openrouter/"):
767
+ if not self.extra_params:
768
+ self.extra_params = {}
769
+ if "extra_body" not in self.extra_params:
770
+ self.extra_params["extra_body"] = {}
771
+ self.extra_params["extra_body"]["reasoning"] = {"effort": effort}
772
+ else:
773
+ if not self.extra_params:
774
+ self.extra_params = {}
775
+ if "extra_body" not in self.extra_params:
776
+ self.extra_params["extra_body"] = {}
777
+ self.extra_params["extra_body"]["reasoning_effort"] = effort
778
+
779
+ def parse_token_value(self, value):
780
+ """
781
+ Parse a token value string into an integer.
782
+ Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
783
+
784
+ Args:
785
+ value: String or int token value
786
+
787
+ Returns:
788
+ Integer token value
789
+ """
790
+ if isinstance(value, int):
791
+ return value
792
+
793
+ if not isinstance(value, str):
794
+ return int(value) # Try to convert to int
795
+
796
+ value = value.strip().upper()
797
+
798
+ if value.endswith("K"):
799
+ multiplier = 1024
800
+ value = value[:-1]
801
+ elif value.endswith("M"):
802
+ multiplier = 1024 * 1024
803
+ value = value[:-1]
804
+ else:
805
+ multiplier = 1
806
+
807
+ # Convert to float first to handle decimal values like "10.5k"
808
+ return int(float(value) * multiplier)
809
+
810
+ def set_thinking_tokens(self, value):
811
+ """
812
+ Set the thinking token budget for models that support it.
813
+ Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
814
+ Pass "0" to disable thinking tokens.
815
+ """
816
+ if value is not None:
817
+ num_tokens = self.parse_token_value(value)
818
+ self.use_temperature = False
819
+ if not self.extra_params:
820
+ self.extra_params = {}
821
+
822
+ # OpenRouter models use 'reasoning' instead of 'thinking'
823
+ if self.name.startswith("openrouter/"):
824
+ if "extra_body" not in self.extra_params:
825
+ self.extra_params["extra_body"] = {}
826
+ if num_tokens > 0:
827
+ self.extra_params["extra_body"]["reasoning"] = {"max_tokens": num_tokens}
828
+ else:
829
+ if "reasoning" in self.extra_params["extra_body"]:
830
+ del self.extra_params["extra_body"]["reasoning"]
831
+ else:
832
+ if num_tokens > 0:
833
+ self.extra_params["thinking"] = {"type": "enabled", "budget_tokens": num_tokens}
834
+ else:
835
+ if "thinking" in self.extra_params:
836
+ del self.extra_params["thinking"]
837
+
838
+ def get_raw_thinking_tokens(self):
839
+ """Get formatted thinking token budget if available"""
840
+ budget = None
841
+
842
+ if self.extra_params:
843
+ # Check for OpenRouter reasoning format
844
+ if self.name.startswith("openrouter/"):
845
+ if (
846
+ "extra_body" in self.extra_params
847
+ and "reasoning" in self.extra_params["extra_body"]
848
+ and "max_tokens" in self.extra_params["extra_body"]["reasoning"]
849
+ ):
850
+ budget = self.extra_params["extra_body"]["reasoning"]["max_tokens"]
851
+ # Check for standard thinking format
852
+ elif (
853
+ "thinking" in self.extra_params and "budget_tokens" in self.extra_params["thinking"]
854
+ ):
855
+ budget = self.extra_params["thinking"]["budget_tokens"]
856
+
857
+ return budget
858
+
859
+ def get_thinking_tokens(self):
860
+ budget = self.get_raw_thinking_tokens()
861
+
862
+ if budget is not None:
863
+ # Format as xx.yK for thousands, xx.yM for millions
864
+ if budget >= 1024 * 1024:
865
+ value = budget / (1024 * 1024)
866
+ if value == int(value):
867
+ return f"{int(value)}M"
868
+ else:
869
+ return f"{value:.1f}M"
870
+ else:
871
+ value = budget / 1024
872
+ if value == int(value):
873
+ return f"{int(value)}k"
874
+ else:
875
+ return f"{value:.1f}k"
876
+ return None
877
+
878
+ def get_reasoning_effort(self):
879
+ """Get reasoning effort value if available"""
880
+ if self.extra_params:
881
+ # Check for OpenRouter reasoning format
882
+ if self.name.startswith("openrouter/"):
883
+ if (
884
+ "extra_body" in self.extra_params
885
+ and "reasoning" in self.extra_params["extra_body"]
886
+ and "effort" in self.extra_params["extra_body"]["reasoning"]
887
+ ):
888
+ return self.extra_params["extra_body"]["reasoning"]["effort"]
889
+ # Check for standard reasoning_effort format (e.g. in extra_body)
890
+ elif (
891
+ "extra_body" in self.extra_params
892
+ and "reasoning_effort" in self.extra_params["extra_body"]
893
+ ):
894
+ return self.extra_params["extra_body"]["reasoning_effort"]
895
+ return None
896
+
897
+ def is_deepseek_r1(self):
898
+ name = self.name.lower()
899
+ if "deepseek" not in name:
900
+ return
901
+ return "r1" in name or "reasoner" in name
902
+
903
+ def is_ollama(self):
904
+ return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/")
905
+
906
+ def github_copilot_token_to_open_ai_key(self, extra_headers):
907
+ # check to see if there's an openai api key
908
+ # If so, check to see if it's expire
909
+ openai_api_key = "OPENAI_API_KEY"
910
+
911
+ if openai_api_key not in os.environ or (
912
+ int(dict(x.split("=") for x in os.environ[openai_api_key].split(";"))["exp"])
913
+ < int(datetime.now().timestamp())
914
+ ):
915
+ import requests
916
+
917
+ class GitHubCopilotTokenError(Exception):
918
+ """Custom exception for GitHub Copilot token-related errors."""
919
+
920
+ pass
921
+
922
+ # Validate GitHub Copilot token exists
923
+ if "GITHUB_COPILOT_TOKEN" not in os.environ:
924
+ raise KeyError("GITHUB_COPILOT_TOKEN environment variable not found")
925
+
926
+ github_token = os.environ["GITHUB_COPILOT_TOKEN"]
927
+ if not github_token.strip():
928
+ raise KeyError("GITHUB_COPILOT_TOKEN environment variable is empty")
929
+
930
+ headers = {
931
+ "Authorization": f"Bearer {os.environ['GITHUB_COPILOT_TOKEN']}",
932
+ "Editor-Version": extra_headers["Editor-Version"],
933
+ "Copilot-Integration-Id": extra_headers["Copilot-Integration-Id"],
934
+ "Content-Type": "application/json",
935
+ }
936
+
937
+ url = "https://api.github.com/copilot_internal/v2/token"
938
+ res = requests.get(url, headers=headers)
939
+ if res.status_code != 200:
940
+ safe_headers = {k: v for k, v in headers.items() if k != "Authorization"}
941
+ token_preview = github_token[:5] + "..." if len(github_token) >= 5 else github_token
942
+ safe_headers["Authorization"] = f"Bearer {token_preview}"
943
+ raise GitHubCopilotTokenError(
944
+ f"GitHub Copilot API request failed (Status: {res.status_code})\n"
945
+ f"URL: {url}\n"
946
+ f"Headers: {json.dumps(safe_headers, indent=2)}\n"
947
+ f"JSON: {res.text}"
948
+ )
949
+
950
+ response_data = res.json()
951
+ token = response_data.get("token")
952
+ if not token:
953
+ raise GitHubCopilotTokenError("Response missing 'token' field")
954
+
955
+ os.environ[openai_api_key] = token
956
+
957
+ def send_completion(
958
+ self, messages, functions, stream, temperature=None, tools=None, max_tokens=None
959
+ ):
960
+ if os.environ.get("AIDER_SANITY_CHECK_TURNS"):
961
+ sanity_check_messages(messages)
962
+
963
+ if self.is_deepseek_r1():
964
+ messages = ensure_alternating_roles(messages)
965
+
966
+ kwargs = dict(model=self.name, stream=stream)
967
+
968
+ if self.use_temperature is not False:
969
+ if temperature is None:
970
+ if isinstance(self.use_temperature, bool):
971
+ temperature = 0
972
+ else:
973
+ temperature = float(self.use_temperature)
974
+
975
+ kwargs["temperature"] = temperature
976
+
977
+ # `tools` is for modern tool usage. `functions` is for legacy/forced calls.
978
+ # If `tools` is provided, it's the canonical list. If not, use `functions`.
979
+ # This handles `base_coder` sending both with same content for `navigator_coder`.
980
+ effective_tools = tools if tools is not None else functions
981
+
982
+ if effective_tools:
983
+ # Check if we have legacy format functions (which lack a 'type' key) and convert them.
984
+ # This is a simplifying assumption that works for aider's use cases.
985
+ is_legacy = any("type" not in tool for tool in effective_tools)
986
+ if is_legacy:
987
+ kwargs["tools"] = [dict(type="function", function=tool) for tool in effective_tools]
988
+ else:
989
+ kwargs["tools"] = effective_tools
990
+ else:
991
+ kwargs["tools"] = []
992
+
993
+ # Forcing a function call is for legacy style `functions` with a single function.
994
+ # This is used by ArchitectCoder and not intended for NavigatorCoder's tools.
995
+ if functions and len(functions) == 1:
996
+ function = functions[0]
997
+ is_legacy = "type" not in function
998
+
999
+ if is_legacy and "name" in function:
1000
+ tool_name = function.get("name")
1001
+ if tool_name:
1002
+ kwargs["tool_choice"] = {"type": "function", "function": {"name": tool_name}}
1003
+
1004
+ if self.extra_params:
1005
+ kwargs.update(self.extra_params)
1006
+
1007
+ if max_tokens:
1008
+ kwargs["max_tokens"] = max_tokens
1009
+
1010
+ if "max_tokens" in kwargs and kwargs["max_tokens"]:
1011
+ kwargs["max_completion_tokens"] = kwargs.pop("max_tokens")
1012
+ if self.is_ollama() and "num_ctx" not in kwargs:
1013
+ num_ctx = int(self.token_count(messages) * 1.25) + 8192
1014
+ kwargs["num_ctx"] = num_ctx
1015
+
1016
+ key = json.dumps(kwargs, sort_keys=True).encode()
1017
+ # dump(kwargs)
1018
+
1019
+ hash_object = hashlib.sha1(key)
1020
+ if "timeout" not in kwargs:
1021
+ kwargs["timeout"] = request_timeout
1022
+ if self.verbose:
1023
+ dump(kwargs)
1024
+ kwargs["messages"] = messages
1025
+
1026
+ # Are we using github copilot?
1027
+ if "GITHUB_COPILOT_TOKEN" in os.environ:
1028
+ if "extra_headers" not in kwargs:
1029
+ kwargs["extra_headers"] = {
1030
+ "Editor-Version": f"aider/{__version__}",
1031
+ "Copilot-Integration-Id": "vscode-chat",
1032
+ }
1033
+
1034
+ self.github_copilot_token_to_open_ai_key(kwargs["extra_headers"])
1035
+
1036
+ try:
1037
+ res = litellm.completion(**kwargs)
1038
+ except Exception as err:
1039
+ res = "Model API Response Error. Please retry the previous request"
1040
+
1041
+ if self.verbose:
1042
+ print(f"LiteLLM API Error: {str(err)}")
1043
+
1044
+ return hash_object, res
1045
+
1046
+ def simple_send_with_retries(self, messages, max_tokens=None):
1047
+ from aider.exceptions import LiteLLMExceptions
1048
+
1049
+ litellm_ex = LiteLLMExceptions()
1050
+ if "deepseek-reasoner" in self.name:
1051
+ messages = ensure_alternating_roles(messages)
1052
+ retry_delay = 0.125
1053
+
1054
+ if self.verbose:
1055
+ dump(messages)
1056
+
1057
+ while True:
1058
+ try:
1059
+ _hash, response = self.send_completion(
1060
+ messages=messages,
1061
+ functions=None,
1062
+ stream=False,
1063
+ max_tokens=max_tokens,
1064
+ )
1065
+ if not response or not hasattr(response, "choices") or not response.choices:
1066
+ return None
1067
+ res = response.choices[0].message.content
1068
+ from aider.reasoning_tags import remove_reasoning_content
1069
+
1070
+ return remove_reasoning_content(res, self.reasoning_tag)
1071
+
1072
+ except litellm_ex.exceptions_tuple() as err:
1073
+ ex_info = litellm_ex.get_ex_info(err)
1074
+ print(str(err))
1075
+ if ex_info.description:
1076
+ print(ex_info.description)
1077
+ should_retry = ex_info.retry
1078
+ if should_retry:
1079
+ retry_delay *= 2
1080
+ if retry_delay > RETRY_TIMEOUT:
1081
+ should_retry = False
1082
+ if not should_retry:
1083
+ return None
1084
+ print(f"Retrying in {retry_delay:.1f} seconds...")
1085
+ time.sleep(retry_delay)
1086
+ continue
1087
+ except AttributeError:
1088
+ return None
1089
+
1090
+
1091
+ def register_models(model_settings_fnames):
1092
+ files_loaded = []
1093
+ for model_settings_fname in model_settings_fnames:
1094
+ if not os.path.exists(model_settings_fname):
1095
+ continue
1096
+
1097
+ if not Path(model_settings_fname).read_text().strip():
1098
+ continue
1099
+
1100
+ try:
1101
+ with open(model_settings_fname, "r") as model_settings_file:
1102
+ model_settings_list = yaml.safe_load(model_settings_file)
1103
+
1104
+ for model_settings_dict in model_settings_list:
1105
+ model_settings = ModelSettings(**model_settings_dict)
1106
+
1107
+ # Remove all existing settings for this model name
1108
+ MODEL_SETTINGS[:] = [ms for ms in MODEL_SETTINGS if ms.name != model_settings.name]
1109
+ # Add the new settings
1110
+ MODEL_SETTINGS.append(model_settings)
1111
+ except Exception as e:
1112
+ raise Exception(f"Error loading model settings from {model_settings_fname}: {e}")
1113
+ files_loaded.append(model_settings_fname)
1114
+
1115
+ return files_loaded
1116
+
1117
+
1118
+ def register_litellm_models(model_fnames):
1119
+ files_loaded = []
1120
+ for model_fname in model_fnames:
1121
+ if not os.path.exists(model_fname):
1122
+ continue
1123
+
1124
+ try:
1125
+ data = Path(model_fname).read_text()
1126
+ if not data.strip():
1127
+ continue
1128
+ model_def = json5.loads(data)
1129
+ if not model_def:
1130
+ continue
1131
+
1132
+ # Defer registration with litellm to faster path.
1133
+ model_info_manager.local_model_metadata.update(model_def)
1134
+ except Exception as e:
1135
+ raise Exception(f"Error loading model definition from {model_fname}: {e}")
1136
+
1137
+ files_loaded.append(model_fname)
1138
+
1139
+ return files_loaded
1140
+
1141
+
1142
+ def validate_variables(vars):
1143
+ missing = []
1144
+ for var in vars:
1145
+ if var not in os.environ:
1146
+ missing.append(var)
1147
+ if missing:
1148
+ return dict(keys_in_environment=False, missing_keys=missing)
1149
+ return dict(keys_in_environment=True, missing_keys=missing)
1150
+
1151
+
1152
+ def sanity_check_models(io, main_model):
1153
+ problem_main = sanity_check_model(io, main_model)
1154
+
1155
+ problem_weak = None
1156
+ if main_model.weak_model and main_model.weak_model is not main_model:
1157
+ problem_weak = sanity_check_model(io, main_model.weak_model)
1158
+
1159
+ problem_editor = None
1160
+ if (
1161
+ main_model.editor_model
1162
+ and main_model.editor_model is not main_model
1163
+ and main_model.editor_model is not main_model.weak_model
1164
+ ):
1165
+ problem_editor = sanity_check_model(io, main_model.editor_model)
1166
+
1167
+ return problem_main or problem_weak or problem_editor
1168
+
1169
+
1170
+ def sanity_check_model(io, model):
1171
+ show = False
1172
+
1173
+ if model.missing_keys:
1174
+ show = True
1175
+ io.tool_warning(f"Warning: {model} expects these environment variables")
1176
+ for key in model.missing_keys:
1177
+ value = os.environ.get(key, "")
1178
+ status = "Set" if value else "Not set"
1179
+ io.tool_output(f"- {key}: {status}")
1180
+
1181
+ if platform.system() == "Windows":
1182
+ io.tool_output(
1183
+ "Note: You may need to restart your terminal or command prompt for `setx` to take"
1184
+ " effect."
1185
+ )
1186
+
1187
+ elif not model.keys_in_environment:
1188
+ show = True
1189
+ io.tool_warning(f"Warning for {model}: Unknown which environment variables are required.")
1190
+
1191
+ # Check for model-specific dependencies
1192
+ check_for_dependencies(io, model.name)
1193
+
1194
+ if not model.info:
1195
+ show = True
1196
+ io.tool_warning(
1197
+ f"Warning for {model}: Unknown context window size and costs, using sane defaults."
1198
+ )
1199
+
1200
+ possible_matches = fuzzy_match_models(model.name)
1201
+ if possible_matches:
1202
+ io.tool_output("Did you mean one of these?")
1203
+ for match in possible_matches:
1204
+ io.tool_output(f"- {match}")
1205
+
1206
+ return show
1207
+
1208
+
1209
+ def check_for_dependencies(io, model_name):
1210
+ """
1211
+ Check for model-specific dependencies and install them if needed.
1212
+
1213
+ Args:
1214
+ io: The IO object for user interaction
1215
+ model_name: The name of the model to check dependencies for
1216
+ """
1217
+ # Check if this is a Bedrock model and ensure boto3 is installed
1218
+ if model_name.startswith("bedrock/"):
1219
+ check_pip_install_extra(
1220
+ io, "boto3", "AWS Bedrock models require the boto3 package.", ["boto3"]
1221
+ )
1222
+
1223
+ # Check if this is a Vertex AI model and ensure google-cloud-aiplatform is installed
1224
+ elif model_name.startswith("vertex_ai/"):
1225
+ check_pip_install_extra(
1226
+ io,
1227
+ "google.cloud.aiplatform",
1228
+ "Google Vertex AI models require the google-cloud-aiplatform package.",
1229
+ ["google-cloud-aiplatform"],
1230
+ )
1231
+
1232
+
1233
+ def fuzzy_match_models(name):
1234
+ name = name.lower()
1235
+
1236
+ chat_models = set()
1237
+ model_metadata = list(litellm.model_cost.items())
1238
+ model_metadata += list(model_info_manager.local_model_metadata.items())
1239
+
1240
+ for orig_model, attrs in model_metadata:
1241
+ model = orig_model.lower()
1242
+ if attrs.get("mode") != "chat":
1243
+ continue
1244
+ provider = attrs.get("litellm_provider", "").lower()
1245
+ if not provider:
1246
+ continue
1247
+ provider += "/"
1248
+
1249
+ if model.startswith(provider):
1250
+ fq_model = orig_model
1251
+ else:
1252
+ fq_model = provider + orig_model
1253
+
1254
+ chat_models.add(fq_model)
1255
+ chat_models.add(orig_model)
1256
+
1257
+ chat_models = sorted(chat_models)
1258
+ # exactly matching model
1259
+ # matching_models = [
1260
+ # (fq,m) for fq,m in chat_models
1261
+ # if name == fq or name == m
1262
+ # ]
1263
+ # if matching_models:
1264
+ # return matching_models
1265
+
1266
+ # Check for model names containing the name
1267
+ matching_models = [m for m in chat_models if name in m]
1268
+ if matching_models:
1269
+ return sorted(set(matching_models))
1270
+
1271
+ # Check for slight misspellings
1272
+ models = set(chat_models)
1273
+ matching_models = difflib.get_close_matches(name, models, n=3, cutoff=0.8)
1274
+
1275
+ return sorted(set(matching_models))
1276
+
1277
+
1278
+ def print_matching_models(io, search):
1279
+ matches = fuzzy_match_models(search)
1280
+ if matches:
1281
+ io.tool_output(f'Models which match "{search}":')
1282
+ for model in matches:
1283
+ io.tool_output(f"- {model}")
1284
+ else:
1285
+ io.tool_output(f'No models match "{search}".')
1286
+
1287
+
1288
+ def get_model_settings_as_yaml():
1289
+ from dataclasses import fields
1290
+
1291
+ import yaml
1292
+
1293
+ model_settings_list = []
1294
+ # Add default settings first with all field values
1295
+ defaults = {}
1296
+ for field in fields(ModelSettings):
1297
+ defaults[field.name] = field.default
1298
+ defaults["name"] = "(default values)"
1299
+ model_settings_list.append(defaults)
1300
+
1301
+ # Sort model settings by name
1302
+ for ms in sorted(MODEL_SETTINGS, key=lambda x: x.name):
1303
+ # Create dict with explicit field order
1304
+ model_settings_dict = {}
1305
+ for field in fields(ModelSettings):
1306
+ value = getattr(ms, field.name)
1307
+ if value != field.default:
1308
+ model_settings_dict[field.name] = value
1309
+ model_settings_list.append(model_settings_dict)
1310
+ # Add blank line between entries
1311
+ model_settings_list.append(None)
1312
+
1313
+ # Filter out None values before dumping
1314
+ yaml_str = yaml.dump(
1315
+ [ms for ms in model_settings_list if ms is not None],
1316
+ default_flow_style=False,
1317
+ sort_keys=False, # Preserve field order from dataclass
1318
+ )
1319
+ # Add actual blank lines between entries
1320
+ return yaml_str.replace("\n- ", "\n\n- ")
1321
+
1322
+
1323
+ def main():
1324
+ if len(sys.argv) < 2:
1325
+ print("Usage: python models.py <model_name> or python models.py --yaml")
1326
+ sys.exit(1)
1327
+
1328
+ if sys.argv[1] == "--yaml":
1329
+ yaml_string = get_model_settings_as_yaml()
1330
+ print(yaml_string)
1331
+ else:
1332
+ model_name = sys.argv[1]
1333
+ matching_models = fuzzy_match_models(model_name)
1334
+
1335
+ if matching_models:
1336
+ print(f"Matching models for '{model_name}':")
1337
+ for model in matching_models:
1338
+ print(model)
1339
+ else:
1340
+ print(f"No matching models found for '{model_name}'.")
1341
+
1342
+
1343
+ if __name__ == "__main__":
1344
+ main()