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/models.py ADDED
@@ -0,0 +1,1313 @@
1
+ import asyncio
2
+ import difflib
3
+ import hashlib
4
+ import importlib.resources
5
+ import json
6
+ import math
7
+ import os
8
+ import platform
9
+ import sys
10
+ import time
11
+ from dataclasses import dataclass, fields
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(self):
898
+ name = self.name.lower()
899
+ if "deepseek" not in name:
900
+ return
901
+ return True
902
+
903
+ def is_ollama(self):
904
+ return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/")
905
+
906
+ async def send_completion(
907
+ self, messages, functions, stream, temperature=None, tools=None, max_tokens=None
908
+ ):
909
+ if os.environ.get("AIDER_SANITY_CHECK_TURNS"):
910
+ sanity_check_messages(messages)
911
+
912
+ messages = ensure_alternating_roles(messages)
913
+
914
+ if self.verbose:
915
+ for message in messages:
916
+ msg_role = message.get("role")
917
+ msg_content = message.get("content") if message.get("content") else ""
918
+ msg_trunc = ""
919
+
920
+ if message.get("content"):
921
+ msg_trunc = message.get("content")[:30]
922
+
923
+ print(f"{msg_role} ({len(msg_content)}): {msg_trunc}")
924
+
925
+ kwargs = dict(model=self.name, stream=stream)
926
+
927
+ if self.use_temperature is not False:
928
+ if temperature is None:
929
+ if isinstance(self.use_temperature, bool):
930
+ temperature = 0
931
+ else:
932
+ temperature = float(self.use_temperature)
933
+
934
+ kwargs["temperature"] = temperature
935
+
936
+ # `tools` is for modern tool usage. `functions` is for legacy/forced calls.
937
+ # This handles `base_coder` sending both with same content for `agent_coder`.
938
+ effective_tools = tools
939
+
940
+ if effective_tools is None and functions:
941
+ # Convert legacy `functions` to `tools` format if `tools` isn't provided.
942
+ effective_tools = [dict(type="function", function=f) for f in functions]
943
+
944
+ if effective_tools:
945
+ kwargs["tools"] = effective_tools
946
+
947
+ # Forcing a function call is for legacy style `functions` with a single function.
948
+ # This is used by ArchitectCoder and not intended for AgentCoder's tools.
949
+ if functions and len(functions) == 1:
950
+ function = functions[0]
951
+
952
+ if "name" in function:
953
+ tool_name = function.get("name")
954
+ if tool_name:
955
+ kwargs["tool_choice"] = {"type": "function", "function": {"name": tool_name}}
956
+
957
+ if self.extra_params:
958
+ kwargs.update(self.extra_params)
959
+
960
+ if max_tokens:
961
+ kwargs["max_tokens"] = max_tokens
962
+
963
+ if "max_tokens" in kwargs and kwargs["max_tokens"]:
964
+ kwargs["max_completion_tokens"] = kwargs.pop("max_tokens")
965
+ if self.is_ollama() and "num_ctx" not in kwargs:
966
+ num_ctx = int(self.token_count(messages) * 1.25) + 8192
967
+ kwargs["num_ctx"] = num_ctx
968
+
969
+ key = json.dumps(kwargs, sort_keys=True).encode()
970
+ # dump(kwargs)
971
+
972
+ hash_object = hashlib.sha1(key)
973
+ if "timeout" not in kwargs:
974
+ kwargs["timeout"] = request_timeout
975
+ if self.verbose:
976
+ dump(kwargs)
977
+ kwargs["messages"] = messages
978
+
979
+ # Are we using github copilot?
980
+ if "GITHUB_COPILOT_TOKEN" in os.environ or self.name.startswith("github_copilot/"):
981
+ if "extra_headers" not in kwargs:
982
+ kwargs["extra_headers"] = {
983
+ "Editor-Version": f"aider/{__version__}",
984
+ "Copilot-Integration-Id": "vscode-chat",
985
+ }
986
+
987
+ try:
988
+ res = await litellm.acompletion(**kwargs)
989
+ except Exception as err:
990
+ print(f"LiteLLM API Error: {str(err)}")
991
+ res = self.model_error_response()
992
+
993
+ if self.verbose:
994
+ print(f"LiteLLM API Error: {str(err)}")
995
+ raise
996
+
997
+ return hash_object, res
998
+
999
+ async def simple_send_with_retries(self, messages, max_tokens=None):
1000
+ from aider.exceptions import LiteLLMExceptions
1001
+
1002
+ litellm_ex = LiteLLMExceptions()
1003
+ if "deepseek-reasoner" in self.name:
1004
+ messages = ensure_alternating_roles(messages)
1005
+ retry_delay = 0.125
1006
+
1007
+ if self.verbose:
1008
+ dump(messages)
1009
+
1010
+ while True:
1011
+ try:
1012
+ _hash, response = await self.send_completion(
1013
+ messages=messages,
1014
+ functions=None,
1015
+ stream=False,
1016
+ max_tokens=max_tokens,
1017
+ )
1018
+ if not response or not hasattr(response, "choices") or not response.choices:
1019
+ return None
1020
+ res = response.choices[0].message.content
1021
+ from aider.reasoning_tags import remove_reasoning_content
1022
+
1023
+ return remove_reasoning_content(res, self.reasoning_tag)
1024
+
1025
+ except litellm_ex.exceptions_tuple() as err:
1026
+ ex_info = litellm_ex.get_ex_info(err)
1027
+ print(str(err))
1028
+ if ex_info.description:
1029
+ print(ex_info.description)
1030
+ should_retry = ex_info.retry
1031
+ if should_retry:
1032
+ retry_delay *= 2
1033
+ if retry_delay > RETRY_TIMEOUT:
1034
+ should_retry = False
1035
+ if not should_retry:
1036
+ return None
1037
+ print(f"Retrying in {retry_delay:.1f} seconds...")
1038
+ time.sleep(retry_delay)
1039
+ continue
1040
+ except AttributeError:
1041
+ return None
1042
+
1043
+ async def model_error_response(self):
1044
+ for i in range(1):
1045
+ await asyncio.sleep(0.1)
1046
+ yield litellm.ModelResponse(
1047
+ choices=[
1048
+ litellm.Choices(
1049
+ finish_reason="stop",
1050
+ index=0,
1051
+ message=litellm.Message(
1052
+ content="Model API Response Error. Please retry the previous request"
1053
+ ), # Provide an empty message object
1054
+ )
1055
+ ],
1056
+ model=self.name,
1057
+ )
1058
+
1059
+
1060
+ def register_models(model_settings_fnames):
1061
+ files_loaded = []
1062
+ for model_settings_fname in model_settings_fnames:
1063
+ if not os.path.exists(model_settings_fname):
1064
+ continue
1065
+
1066
+ if not Path(model_settings_fname).read_text().strip():
1067
+ continue
1068
+
1069
+ try:
1070
+ with open(model_settings_fname, "r") as model_settings_file:
1071
+ model_settings_list = yaml.safe_load(model_settings_file)
1072
+
1073
+ for model_settings_dict in model_settings_list:
1074
+ model_settings = ModelSettings(**model_settings_dict)
1075
+
1076
+ # Remove all existing settings for this model name
1077
+ MODEL_SETTINGS[:] = [ms for ms in MODEL_SETTINGS if ms.name != model_settings.name]
1078
+ # Add the new settings
1079
+ MODEL_SETTINGS.append(model_settings)
1080
+ except Exception as e:
1081
+ raise Exception(f"Error loading model settings from {model_settings_fname}: {e}")
1082
+ files_loaded.append(model_settings_fname)
1083
+
1084
+ return files_loaded
1085
+
1086
+
1087
+ def register_litellm_models(model_fnames):
1088
+ files_loaded = []
1089
+ for model_fname in model_fnames:
1090
+ if not os.path.exists(model_fname):
1091
+ continue
1092
+
1093
+ try:
1094
+ data = Path(model_fname).read_text()
1095
+ if not data.strip():
1096
+ continue
1097
+ model_def = json5.loads(data)
1098
+ if not model_def:
1099
+ continue
1100
+
1101
+ # Defer registration with litellm to faster path.
1102
+ model_info_manager.local_model_metadata.update(model_def)
1103
+ except Exception as e:
1104
+ raise Exception(f"Error loading model definition from {model_fname}: {e}")
1105
+
1106
+ files_loaded.append(model_fname)
1107
+
1108
+ return files_loaded
1109
+
1110
+
1111
+ def validate_variables(vars):
1112
+ missing = []
1113
+ for var in vars:
1114
+ if var not in os.environ:
1115
+ missing.append(var)
1116
+ if missing:
1117
+ return dict(keys_in_environment=False, missing_keys=missing)
1118
+ return dict(keys_in_environment=True, missing_keys=missing)
1119
+
1120
+
1121
+ async def sanity_check_models(io, main_model):
1122
+ problem_main = await sanity_check_model(io, main_model)
1123
+
1124
+ problem_weak = None
1125
+ if main_model.weak_model and main_model.weak_model is not main_model:
1126
+ problem_weak = await sanity_check_model(io, main_model.weak_model)
1127
+
1128
+ problem_editor = None
1129
+ if (
1130
+ main_model.editor_model
1131
+ and main_model.editor_model is not main_model
1132
+ and main_model.editor_model is not main_model.weak_model
1133
+ ):
1134
+ problem_editor = await sanity_check_model(io, main_model.editor_model)
1135
+
1136
+ return problem_main or problem_weak or problem_editor
1137
+
1138
+
1139
+ async def sanity_check_model(io, model):
1140
+ show = False
1141
+
1142
+ if model.missing_keys:
1143
+ show = True
1144
+ io.tool_warning(f"Warning: {model} expects these environment variables")
1145
+ for key in model.missing_keys:
1146
+ value = os.environ.get(key, "")
1147
+ status = "Set" if value else "Not set"
1148
+ io.tool_output(f"- {key}: {status}")
1149
+
1150
+ if platform.system() == "Windows":
1151
+ io.tool_output(
1152
+ "Note: You may need to restart your terminal or command prompt for `setx` to take"
1153
+ " effect."
1154
+ )
1155
+
1156
+ elif not model.keys_in_environment:
1157
+ show = True
1158
+ io.tool_warning(f"Warning for {model}: Unknown which environment variables are required.")
1159
+
1160
+ # Check for model-specific dependencies
1161
+ await check_for_dependencies(io, model.name)
1162
+
1163
+ if not model.info:
1164
+ show = True
1165
+ io.tool_warning(
1166
+ f"Warning for {model}: Unknown context window size and costs, using sane defaults."
1167
+ )
1168
+
1169
+ possible_matches = fuzzy_match_models(model.name)
1170
+ if possible_matches:
1171
+ io.tool_output("Did you mean one of these?")
1172
+ for match in possible_matches:
1173
+ io.tool_output(f"- {match}")
1174
+
1175
+ return show
1176
+
1177
+
1178
+ async def check_for_dependencies(io, model_name):
1179
+ """
1180
+ Check for model-specific dependencies and install them if needed.
1181
+
1182
+ Args:
1183
+ io: The IO object for user interaction
1184
+ model_name: The name of the model to check dependencies for
1185
+ """
1186
+ # Check if this is a Bedrock model and ensure boto3 is installed
1187
+ if model_name.startswith("bedrock/"):
1188
+ await check_pip_install_extra(
1189
+ io, "boto3", "AWS Bedrock models require the boto3 package.", ["boto3"]
1190
+ )
1191
+
1192
+ # Check if this is a Vertex AI model and ensure google-cloud-aiplatform is installed
1193
+ elif model_name.startswith("vertex_ai/"):
1194
+ await check_pip_install_extra(
1195
+ io,
1196
+ "google.cloud.aiplatform",
1197
+ "Google Vertex AI models require the google-cloud-aiplatform package.",
1198
+ ["google-cloud-aiplatform"],
1199
+ )
1200
+
1201
+
1202
+ def fuzzy_match_models(name):
1203
+ name = name.lower()
1204
+
1205
+ chat_models = set()
1206
+ model_metadata = list(litellm.model_cost.items())
1207
+ model_metadata += list(model_info_manager.local_model_metadata.items())
1208
+
1209
+ for orig_model, attrs in model_metadata:
1210
+ model = orig_model.lower()
1211
+ if attrs.get("mode") != "chat":
1212
+ continue
1213
+ provider = attrs.get("litellm_provider", "").lower()
1214
+ if not provider:
1215
+ continue
1216
+ provider += "/"
1217
+
1218
+ if model.startswith(provider):
1219
+ fq_model = orig_model
1220
+ else:
1221
+ fq_model = provider + orig_model
1222
+
1223
+ chat_models.add(fq_model)
1224
+ chat_models.add(orig_model)
1225
+
1226
+ chat_models = sorted(chat_models)
1227
+ # exactly matching model
1228
+ # matching_models = [
1229
+ # (fq,m) for fq,m in chat_models
1230
+ # if name == fq or name == m
1231
+ # ]
1232
+ # if matching_models:
1233
+ # return matching_models
1234
+
1235
+ # Check for model names containing the name
1236
+ matching_models = [m for m in chat_models if name in m]
1237
+ if matching_models:
1238
+ return sorted(set(matching_models))
1239
+
1240
+ # Check for slight misspellings
1241
+ models = set(chat_models)
1242
+ matching_models = difflib.get_close_matches(name, models, n=3, cutoff=0.8)
1243
+
1244
+ return sorted(set(matching_models))
1245
+
1246
+
1247
+ def print_matching_models(io, search):
1248
+ matches = fuzzy_match_models(search)
1249
+ if matches:
1250
+ io.tool_output(f'Models which match "{search}":')
1251
+ for model in matches:
1252
+ io.tool_output(f"- {model}")
1253
+ else:
1254
+ io.tool_output(f'No models match "{search}".')
1255
+
1256
+
1257
+ def get_model_settings_as_yaml():
1258
+ from dataclasses import fields
1259
+
1260
+ import yaml
1261
+
1262
+ model_settings_list = []
1263
+ # Add default settings first with all field values
1264
+ defaults = {}
1265
+ for field in fields(ModelSettings):
1266
+ defaults[field.name] = field.default
1267
+ defaults["name"] = "(default values)"
1268
+ model_settings_list.append(defaults)
1269
+
1270
+ # Sort model settings by name
1271
+ for ms in sorted(MODEL_SETTINGS, key=lambda x: x.name):
1272
+ # Create dict with explicit field order
1273
+ model_settings_dict = {}
1274
+ for field in fields(ModelSettings):
1275
+ value = getattr(ms, field.name)
1276
+ if value != field.default:
1277
+ model_settings_dict[field.name] = value
1278
+ model_settings_list.append(model_settings_dict)
1279
+ # Add blank line between entries
1280
+ model_settings_list.append(None)
1281
+
1282
+ # Filter out None values before dumping
1283
+ yaml_str = yaml.dump(
1284
+ [ms for ms in model_settings_list if ms is not None],
1285
+ default_flow_style=False,
1286
+ sort_keys=False, # Preserve field order from dataclass
1287
+ )
1288
+ # Add actual blank lines between entries
1289
+ return yaml_str.replace("\n- ", "\n\n- ")
1290
+
1291
+
1292
+ def main():
1293
+ if len(sys.argv) < 2:
1294
+ print("Usage: python models.py <model_name> or python models.py --yaml")
1295
+ sys.exit(1)
1296
+
1297
+ if sys.argv[1] == "--yaml":
1298
+ yaml_string = get_model_settings_as_yaml()
1299
+ print(yaml_string)
1300
+ else:
1301
+ model_name = sys.argv[1]
1302
+ matching_models = fuzzy_match_models(model_name)
1303
+
1304
+ if matching_models:
1305
+ print(f"Matching models for '{model_name}':")
1306
+ for model in matching_models:
1307
+ print(model)
1308
+ else:
1309
+ print(f"No matching models found for '{model_name}'.")
1310
+
1311
+
1312
+ if __name__ == "__main__":
1313
+ main()