cecli-dev 0.95.5__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 (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.95.5.dist-info/METADATA +549 -0
  362. cecli_dev-0.95.5.dist-info/RECORD +366 -0
  363. cecli_dev-0.95.5.dist-info/WHEEL +5 -0
  364. cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.95.5.dist-info/top_level.txt +1 -0
cecli/models.py ADDED
@@ -0,0 +1,1255 @@
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 yaml
16
+ from PIL import Image
17
+
18
+ from cecli import __version__
19
+ from cecli.dump import dump
20
+ from cecli.helpers.file_searcher import handle_core_files
21
+ from cecli.helpers.model_providers import ModelProviderManager
22
+ from cecli.helpers.requests import model_request_parser
23
+ from cecli.llm import litellm
24
+ from cecli.sendchat import sanity_check_messages
25
+ from cecli.utils import check_pip_install_extra
26
+
27
+ RETRY_TIMEOUT = 60
28
+ COPY_PASTE_PREFIX = "cp:"
29
+ request_timeout = 600
30
+ DEFAULT_MODEL_NAME = "gpt-4o"
31
+ ANTHROPIC_BETA_HEADER = "prompt-caching-2024-07-31,pdfs-2024-09-25"
32
+ OPENAI_MODELS = """
33
+ o1
34
+ o1-preview
35
+ o1-mini
36
+ o3-mini
37
+ gpt-4
38
+ gpt-4o
39
+ gpt-4o-2024-05-13
40
+ gpt-4-turbo-preview
41
+ gpt-4-0314
42
+ gpt-4-0613
43
+ gpt-4-32k
44
+ gpt-4-32k-0314
45
+ gpt-4-32k-0613
46
+ gpt-4-turbo
47
+ gpt-4-turbo-2024-04-09
48
+ gpt-4-1106-preview
49
+ gpt-4-0125-preview
50
+ gpt-4-vision-preview
51
+ gpt-4-1106-vision-preview
52
+ gpt-4o-mini
53
+ gpt-4o-mini-2024-07-18
54
+ gpt-3.5-turbo
55
+ gpt-3.5-turbo-0301
56
+ gpt-3.5-turbo-0613
57
+ gpt-3.5-turbo-1106
58
+ gpt-3.5-turbo-0125
59
+ gpt-3.5-turbo-16k
60
+ gpt-3.5-turbo-16k-0613
61
+ """
62
+ OPENAI_MODELS = [ln.strip() for ln in OPENAI_MODELS.splitlines() if ln.strip()]
63
+ ANTHROPIC_MODELS = """
64
+ claude-2
65
+ claude-2.1
66
+ claude-3-haiku-20240307
67
+ claude-3-5-haiku-20241022
68
+ claude-3-opus-20240229
69
+ claude-3-sonnet-20240229
70
+ claude-3-5-sonnet-20240620
71
+ claude-3-5-sonnet-20241022
72
+ claude-sonnet-4-20250514
73
+ claude-opus-4-20250514
74
+ """
75
+ ANTHROPIC_MODELS = [ln.strip() for ln in ANTHROPIC_MODELS.splitlines() if ln.strip()]
76
+ MODEL_ALIASES = {
77
+ "sonnet": "anthropic/claude-sonnet-4-20250514",
78
+ "haiku": "claude-3-5-haiku-20241022",
79
+ "opus": "claude-opus-4-20250514",
80
+ "4": "gpt-4-0613",
81
+ "4o": "gpt-4o",
82
+ "4-turbo": "gpt-4-1106-preview",
83
+ "35turbo": "gpt-3.5-turbo",
84
+ "35-turbo": "gpt-3.5-turbo",
85
+ "3": "gpt-3.5-turbo",
86
+ "deepseek": "deepseek/deepseek-chat",
87
+ "flash": "gemini/gemini-2.5-flash",
88
+ "flash-lite": "gemini/gemini-2.5-flash-lite",
89
+ "quasar": "openrouter/openrouter/quasar-alpha",
90
+ "r1": "deepseek/deepseek-reasoner",
91
+ "gemini-2.5-pro": "gemini/gemini-2.5-pro",
92
+ "gemini-3-pro-preview": "gemini/gemini-3-pro-preview",
93
+ "gemini": "gemini/gemini-3-pro-preview",
94
+ "gemini-exp": "gemini/gemini-2.5-pro-exp-03-25",
95
+ "grok3": "xai/grok-3-beta",
96
+ "optimus": "openrouter/openrouter/optimus-alpha",
97
+ }
98
+
99
+
100
+ @dataclass
101
+ class ModelSettings:
102
+ name: str
103
+ edit_format: str = "diff"
104
+ weak_model_name: Optional[str] = None
105
+ use_repo_map: bool = False
106
+ send_undo_reply: bool = False
107
+ lazy: bool = False
108
+ overeager: bool = False
109
+ reminder: str = "user"
110
+ examples_as_sys_msg: bool = False
111
+ extra_params: Optional[dict] = None
112
+ cache_control: bool = False
113
+ caches_by_default: bool = False
114
+ use_system_prompt: bool = True
115
+ use_temperature: Union[bool, float] = True
116
+ streaming: bool = True
117
+ editor_model_name: Optional[str] = None
118
+ editor_edit_format: Optional[str] = None
119
+ reasoning_tag: Optional[str] = None
120
+ remove_reasoning: Optional[str] = None
121
+ system_prompt_prefix: Optional[str] = None
122
+ accepts_settings: Optional[list] = None
123
+
124
+
125
+ MODEL_SETTINGS = []
126
+ with importlib.resources.open_text("cecli.resources", "model-settings.yml") as f:
127
+ model_settings_list = yaml.safe_load(f)
128
+ for model_settings_dict in model_settings_list:
129
+ MODEL_SETTINGS.append(ModelSettings(**model_settings_dict))
130
+
131
+
132
+ class ModelInfoManager:
133
+ MODEL_INFO_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
134
+ CACHE_TTL = 60 * 60 * 24
135
+
136
+ def __init__(self):
137
+ self.cache_dir = handle_core_files(Path.home() / ".cecli" / "caches")
138
+ self.cache_file = self.cache_dir / "model_prices_and_context_window.json"
139
+ self.content = None
140
+ self.local_model_metadata = {}
141
+ self.verify_ssl = True
142
+ self._cache_loaded = False
143
+ self.provider_manager = ModelProviderManager()
144
+ self.openai_provider_manager = self.provider_manager
145
+
146
+ def set_verify_ssl(self, verify_ssl):
147
+ self.verify_ssl = verify_ssl
148
+ self.provider_manager.set_verify_ssl(verify_ssl)
149
+
150
+ def _load_cache(self):
151
+ if self._cache_loaded:
152
+ return
153
+ try:
154
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
155
+ if self.cache_file.exists():
156
+ cache_age = time.time() - self.cache_file.stat().st_mtime
157
+ if cache_age < self.CACHE_TTL:
158
+ try:
159
+ self.content = json.loads(self.cache_file.read_text())
160
+ except json.JSONDecodeError:
161
+ self.content = None
162
+ except OSError:
163
+ pass
164
+ self._cache_loaded = True
165
+
166
+ def _update_cache(self):
167
+ try:
168
+ import requests
169
+
170
+ response = requests.get(self.MODEL_INFO_URL, timeout=5, verify=self.verify_ssl)
171
+ if response.status_code == 200:
172
+ self.content = response.json()
173
+ try:
174
+ self.cache_file.write_text(json.dumps(self.content, indent=4))
175
+ except OSError:
176
+ pass
177
+ except Exception as ex:
178
+ print(str(ex))
179
+ try:
180
+ self.cache_file.write_text("{}")
181
+ except OSError:
182
+ pass
183
+
184
+ def get_model_from_cached_json_db(self, model):
185
+ data = self.local_model_metadata.get(model)
186
+ if data:
187
+ return data
188
+ self._load_cache()
189
+ if not self.content:
190
+ self._update_cache()
191
+ if not self.content:
192
+ return dict()
193
+ info = self.content.get(model, dict())
194
+ if info:
195
+ return info
196
+ pieces = model.split("/")
197
+ if len(pieces) == 2:
198
+ info = self.content.get(pieces[1])
199
+ if info and info.get("litellm_provider") == pieces[0]:
200
+ return info
201
+ return dict()
202
+
203
+ def get_model_info(self, model):
204
+ cached_info = self.get_model_from_cached_json_db(model)
205
+ litellm_info = None
206
+ if litellm._lazy_module or not cached_info:
207
+ try:
208
+ litellm_info = litellm.get_model_info(model)
209
+ except Exception as ex:
210
+ if "model_prices_and_context_window.json" not in str(ex):
211
+ print(str(ex))
212
+ provider_info = self._resolve_via_provider(model, cached_info)
213
+ if provider_info:
214
+ return provider_info
215
+ if litellm_info:
216
+ return litellm_info
217
+ return cached_info
218
+
219
+ def _resolve_via_provider(self, model, cached_info):
220
+ if cached_info:
221
+ return None
222
+ provider = model.split("/", 1)[0] if "/" in model else None
223
+ if not self.provider_manager.supports_provider(provider):
224
+ return None
225
+ provider_info = self.provider_manager.get_model_info(model)
226
+ if provider_info:
227
+ self._record_dynamic_model(model, provider_info)
228
+ return provider_info
229
+ if provider == "openrouter":
230
+ openrouter_info = self.fetch_openrouter_model_info(model)
231
+ if openrouter_info:
232
+ openrouter_info.setdefault("litellm_provider", "openrouter")
233
+ self._record_dynamic_model(model, openrouter_info)
234
+ return openrouter_info
235
+ return None
236
+
237
+ def _record_dynamic_model(self, model, info):
238
+ self.local_model_metadata[model] = info
239
+ self._ensure_model_settings_entry(model)
240
+
241
+ def _ensure_model_settings_entry(self, model):
242
+ if any(ms.name == model for ms in MODEL_SETTINGS):
243
+ return
244
+ MODEL_SETTINGS.append(ModelSettings(name=model))
245
+
246
+ def fetch_openrouter_model_info(self, model):
247
+ """
248
+ Fetch model info by scraping the openrouter model page.
249
+ Expected URL: https://openrouter.ai/<model_route>
250
+ Example: openrouter/qwen/qwen-2.5-72b-instruct:free
251
+ Returns a dict with keys: max_tokens, max_input_tokens, max_output_tokens,
252
+ input_cost_per_token, output_cost_per_token.
253
+ """
254
+ url_part = model[len("openrouter/") :]
255
+ url = "https://openrouter.ai/" + url_part
256
+ try:
257
+ import requests
258
+
259
+ response = requests.get(url, timeout=5, verify=self.verify_ssl)
260
+ if response.status_code != 200:
261
+ return {}
262
+ html = response.text
263
+ import re
264
+
265
+ if re.search(
266
+ f"The model\\s*.*{re.escape(url_part)}.* is not available", html, re.IGNORECASE
267
+ ):
268
+ print(f"\x1b[91mError: Model '{url_part}' is not available\x1b[0m")
269
+ return {}
270
+ text = re.sub("<[^>]+>", " ", html)
271
+ context_match = re.search("([\\d,]+)\\s*context", text)
272
+ if context_match:
273
+ context_str = context_match.group(1).replace(",", "")
274
+ context_size = int(context_str)
275
+ else:
276
+ context_size = None
277
+ input_cost_match = re.search("\\$\\s*([\\d.]+)\\s*/M input tokens", text, re.IGNORECASE)
278
+ output_cost_match = re.search(
279
+ "\\$\\s*([\\d.]+)\\s*/M output tokens", text, re.IGNORECASE
280
+ )
281
+ input_cost = float(input_cost_match.group(1)) / 1000000 if input_cost_match else None
282
+ output_cost = float(output_cost_match.group(1)) / 1000000 if output_cost_match else None
283
+ if context_size is None or input_cost is None or output_cost is None:
284
+ return {}
285
+ params = {
286
+ "max_input_tokens": context_size,
287
+ "max_tokens": context_size,
288
+ "max_output_tokens": context_size,
289
+ "input_cost_per_token": input_cost,
290
+ "output_cost_per_token": output_cost,
291
+ "litellm_provider": "openrouter",
292
+ }
293
+ return params
294
+ except Exception as e:
295
+ print("Error fetching openrouter info:", str(e))
296
+ return {}
297
+
298
+
299
+ model_info_manager = ModelInfoManager()
300
+
301
+
302
+ class Model(ModelSettings):
303
+ def __init__(
304
+ self,
305
+ model,
306
+ weak_model=None,
307
+ editor_model=None,
308
+ editor_edit_format=None,
309
+ verbose=False,
310
+ io=None,
311
+ override_kwargs=None,
312
+ ):
313
+ provided_model = model or ""
314
+ if isinstance(provided_model, Model):
315
+ provided_model = provided_model.name
316
+ elif not isinstance(provided_model, str):
317
+ provided_model = str(provided_model)
318
+ self.io = io
319
+ self.verbose = verbose
320
+ self.override_kwargs = override_kwargs or {}
321
+ self.copy_paste_mode = False
322
+ self.copy_paste_transport = "api"
323
+ if provided_model.startswith(COPY_PASTE_PREFIX):
324
+ model = provided_model.removeprefix(COPY_PASTE_PREFIX)
325
+ self.enable_copy_paste_mode(transport="clipboard")
326
+ else:
327
+ model = provided_model
328
+ model = MODEL_ALIASES.get(model, model)
329
+ self.name = model
330
+ self.max_chat_history_tokens = 1024
331
+ self.weak_model = None
332
+ self.editor_model = None
333
+ self.extra_model_settings = next(
334
+ (ms for ms in MODEL_SETTINGS if ms.name == "cecli/extra_params"), None
335
+ )
336
+ self.info = self.get_model_info(model)
337
+ self.litellm_provider = (self.info.get("litellm_provider") or "").lower()
338
+ res = self.validate_environment()
339
+ self.missing_keys = res.get("missing_keys")
340
+ self.keys_in_environment = res.get("keys_in_environment")
341
+ max_input_tokens = self.info.get("max_input_tokens") or 0
342
+ self.max_chat_history_tokens = min(max(max_input_tokens / 16, 1024), 8192)
343
+ self.configure_model_settings(model)
344
+ self._apply_provider_defaults()
345
+ self.get_weak_model(weak_model)
346
+ if editor_model is False:
347
+ self.editor_model_name = None
348
+ else:
349
+ self.get_editor_model(editor_model, editor_edit_format)
350
+ if self.copy_paste_transport == "clipboard":
351
+ self.streaming = False
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
+ if self.reasoning_tag is None and self.remove_reasoning is not None:
362
+ self.reasoning_tag = self.remove_reasoning
363
+
364
+ def configure_model_settings(self, model):
365
+ exact_match = False
366
+ for ms in MODEL_SETTINGS:
367
+ if model == ms.name:
368
+ self._copy_fields(ms)
369
+ exact_match = True
370
+ break
371
+ if self.accepts_settings is None:
372
+ self.accepts_settings = []
373
+ model = model.lower()
374
+ if not exact_match:
375
+ self.apply_generic_model_settings(model)
376
+ if (
377
+ self.extra_model_settings
378
+ and self.extra_model_settings.extra_params
379
+ and self.extra_model_settings.name == "cecli/extra_params"
380
+ ):
381
+ if not self.extra_params:
382
+ self.extra_params = {}
383
+ for key, value in self.extra_model_settings.extra_params.items():
384
+ if isinstance(value, dict) and isinstance(self.extra_params.get(key), dict):
385
+ self.extra_params[key] = {**self.extra_params[key], **value}
386
+ else:
387
+ self.extra_params[key] = value
388
+ if self.name.startswith("openrouter/"):
389
+ if self.accepts_settings is None:
390
+ self.accepts_settings = []
391
+ if "thinking_tokens" not in self.accepts_settings:
392
+ self.accepts_settings.append("thinking_tokens")
393
+ if "reasoning_effort" not in self.accepts_settings:
394
+ self.accepts_settings.append("reasoning_effort")
395
+ if self.override_kwargs:
396
+ if not self.extra_params:
397
+ self.extra_params = {}
398
+ for key, value in self.override_kwargs.items():
399
+ if isinstance(value, dict) and isinstance(self.extra_params.get(key), dict):
400
+ self.extra_params[key] = {**self.extra_params[key], **value}
401
+ else:
402
+ self.extra_params[key] = value
403
+
404
+ def apply_generic_model_settings(self, model):
405
+ if "/o3-mini" in model:
406
+ self.edit_format = "diff"
407
+ self.use_repo_map = True
408
+ self.use_temperature = False
409
+ self.system_prompt_prefix = "Formatting re-enabled. "
410
+ self.system_prompt_prefix = "Formatting re-enabled. "
411
+ if "reasoning_effort" not in self.accepts_settings:
412
+ self.accepts_settings.append("reasoning_effort")
413
+ return
414
+ if "gpt-4.1-mini" in model:
415
+ self.edit_format = "diff"
416
+ self.use_repo_map = True
417
+ self.reminder = "sys"
418
+ self.examples_as_sys_msg = False
419
+ return
420
+ if "gpt-4.1" in model:
421
+ self.edit_format = "diff"
422
+ self.use_repo_map = True
423
+ self.reminder = "sys"
424
+ self.examples_as_sys_msg = False
425
+ return
426
+ last_segment = model.split("/")[-1]
427
+ if last_segment in ("gpt-5", "gpt-5-2025-08-07") or "gpt-5.1" in model:
428
+ self.use_temperature = False
429
+ self.edit_format = "diff"
430
+ if "reasoning_effort" not in self.accepts_settings:
431
+ self.accepts_settings.append("reasoning_effort")
432
+ return
433
+ if "/o1-mini" in model:
434
+ self.use_repo_map = True
435
+ self.use_temperature = False
436
+ self.use_system_prompt = False
437
+ return
438
+ if "/o1-preview" in model:
439
+ self.edit_format = "diff"
440
+ self.use_repo_map = True
441
+ self.use_temperature = False
442
+ self.use_system_prompt = False
443
+ return
444
+ if "/o1" in model:
445
+ self.edit_format = "diff"
446
+ self.use_repo_map = True
447
+ self.use_temperature = False
448
+ self.streaming = False
449
+ self.system_prompt_prefix = "Formatting re-enabled. "
450
+ if "reasoning_effort" not in self.accepts_settings:
451
+ self.accepts_settings.append("reasoning_effort")
452
+ return
453
+ if "deepseek" in model and "v3" in model:
454
+ self.edit_format = "diff"
455
+ self.use_repo_map = True
456
+ self.reminder = "sys"
457
+ self.examples_as_sys_msg = True
458
+ return
459
+ if "deepseek" in model and ("r1" in model or "reasoning" in model):
460
+ self.edit_format = "diff"
461
+ self.use_repo_map = True
462
+ self.examples_as_sys_msg = True
463
+ self.use_temperature = False
464
+ self.reasoning_tag = "think"
465
+ return
466
+ if ("llama3" in model or "llama-3" in model) and "70b" in model:
467
+ self.edit_format = "diff"
468
+ self.use_repo_map = True
469
+ self.send_undo_reply = True
470
+ self.examples_as_sys_msg = True
471
+ return
472
+ if "gpt-4-turbo" in model or "gpt-4-" in model and "-preview" in model:
473
+ self.edit_format = "udiff"
474
+ self.use_repo_map = True
475
+ self.send_undo_reply = True
476
+ return
477
+ if "gpt-4" in model or "claude-3-opus" in model:
478
+ self.edit_format = "diff"
479
+ self.use_repo_map = True
480
+ self.send_undo_reply = True
481
+ return
482
+ if "gpt-3.5" in model or "gpt-4" in model:
483
+ self.reminder = "sys"
484
+ return
485
+ if "3-7-sonnet" in model:
486
+ self.edit_format = "diff"
487
+ self.use_repo_map = True
488
+ self.examples_as_sys_msg = True
489
+ self.reminder = "user"
490
+ if "thinking_tokens" not in self.accepts_settings:
491
+ self.accepts_settings.append("thinking_tokens")
492
+ return
493
+ if "3.5-sonnet" in model or "3-5-sonnet" in model:
494
+ self.edit_format = "diff"
495
+ self.use_repo_map = True
496
+ self.examples_as_sys_msg = True
497
+ self.reminder = "user"
498
+ return
499
+ if model.startswith("o1-") or "/o1-" in model:
500
+ self.use_system_prompt = False
501
+ self.use_temperature = False
502
+ return
503
+ if (
504
+ "qwen" in model
505
+ and "coder" in model
506
+ and ("2.5" in model or "2-5" in model)
507
+ and "32b" in model
508
+ ):
509
+ self.edit_format = "diff"
510
+ self.editor_edit_format = "editor-diff"
511
+ self.use_repo_map = True
512
+ return
513
+ if "qwq" in model and "32b" in model and "preview" not in model:
514
+ self.edit_format = "diff"
515
+ self.editor_edit_format = "editor-diff"
516
+ self.use_repo_map = True
517
+ self.reasoning_tag = "think"
518
+ self.examples_as_sys_msg = True
519
+ self.use_temperature = 0.6
520
+ self.extra_params = dict(top_p=0.95)
521
+ return
522
+ if "qwen3" in model:
523
+ self.edit_format = "diff"
524
+ self.use_repo_map = True
525
+ if "235b" in model:
526
+ self.system_prompt_prefix = "/no_think"
527
+ self.use_temperature = 0.7
528
+ self.extra_params = {"top_p": 0.8, "top_k": 20, "min_p": 0.0}
529
+ else:
530
+ self.examples_as_sys_msg = True
531
+ self.use_temperature = 0.6
532
+ self.reasoning_tag = "think"
533
+ self.extra_params = {"top_p": 0.95, "top_k": 20, "min_p": 0.0}
534
+ return
535
+ if self.edit_format == "diff":
536
+ self.use_repo_map = True
537
+ return
538
+
539
+ def __str__(self):
540
+ return self.name
541
+
542
+ def enable_copy_paste_mode(self, *, transport="api"):
543
+ self.copy_paste_mode = True
544
+ self.copy_paste_transport = transport
545
+
546
+ def get_weak_model(self, provided_weak_model):
547
+ if provided_weak_model is False:
548
+ self.weak_model = self
549
+ self.weak_model_name = None
550
+ return
551
+ if self.copy_paste_transport == "clipboard":
552
+ self.weak_model = self
553
+ self.weak_model_name = None
554
+ return
555
+ if isinstance(provided_weak_model, Model):
556
+ self.weak_model = provided_weak_model
557
+ self.weak_model_name = provided_weak_model.name
558
+ return
559
+ if provided_weak_model:
560
+ self.weak_model_name = provided_weak_model
561
+ if not self.weak_model_name:
562
+ self.weak_model = self
563
+ return
564
+ if self.weak_model_name == self.name:
565
+ self.weak_model = self
566
+ return
567
+ self.weak_model = Model(self.weak_model_name, weak_model=False, io=self.io)
568
+ return self.weak_model
569
+
570
+ def commit_message_models(self):
571
+ return [self.weak_model, self]
572
+
573
+ def get_editor_model(self, provided_editor_model, editor_edit_format):
574
+ if self.copy_paste_transport == "clipboard":
575
+ provided_editor_model = False
576
+ self.editor_model_name = self.name
577
+ self.editor_model = self
578
+ if isinstance(provided_editor_model, Model):
579
+ self.editor_model = provided_editor_model
580
+ self.editor_model_name = provided_editor_model.name
581
+ elif provided_editor_model:
582
+ self.editor_model_name = provided_editor_model
583
+ if editor_edit_format:
584
+ self.editor_edit_format = editor_edit_format
585
+ if not self.editor_model_name or self.editor_model_name == self.name:
586
+ self.editor_model = self
587
+ else:
588
+ self.editor_model = Model(self.editor_model_name, editor_model=False, io=self.io)
589
+ if not self.editor_edit_format:
590
+ self.editor_edit_format = self.editor_model.edit_format
591
+ if self.editor_edit_format in ("diff", "whole", "diff-fenced"):
592
+ self.editor_edit_format = "editor-" + self.editor_edit_format
593
+ return self.editor_model
594
+
595
+ def _ensure_extra_params_dict(self):
596
+ if self.extra_params is None:
597
+ self.extra_params = {}
598
+ elif not isinstance(self.extra_params, dict):
599
+ self.extra_params = dict(self.extra_params)
600
+
601
+ def _apply_provider_defaults(self):
602
+ provider = (self.info.get("litellm_provider") or "").lower()
603
+ self.litellm_provider = provider or None
604
+ if not provider:
605
+ return
606
+ provider_config = model_info_manager.provider_manager.get_provider_config(provider)
607
+ if not provider_config:
608
+ return
609
+ self._ensure_extra_params_dict()
610
+ self.extra_params.setdefault("custom_llm_provider", provider)
611
+ if provider_config.get("supports_stream") is False:
612
+ self.streaming = False
613
+ base_url = model_info_manager.provider_manager.get_provider_base_url(provider)
614
+ if base_url:
615
+ self.extra_params.setdefault("base_url", base_url)
616
+ default_headers = provider_config.get("default_headers") or {}
617
+ if default_headers:
618
+ headers = self.extra_params.setdefault("extra_headers", {})
619
+ for key, value in default_headers.items():
620
+ headers.setdefault(key, value)
621
+ provider_extra = provider_config.get("extra_params") or {}
622
+ for key, value in provider_extra.items():
623
+ if key not in self.extra_params:
624
+ self.extra_params[key] = value
625
+
626
+ def tokenizer(self, text):
627
+ return litellm.encode(model=self.name, text=text)
628
+
629
+ def token_count(self, messages):
630
+ if isinstance(messages, dict):
631
+ messages = [messages]
632
+ if isinstance(messages, list):
633
+ try:
634
+ return litellm.token_counter(model=self.name, messages=messages)
635
+ except Exception:
636
+ pass
637
+ if not self.tokenizer:
638
+ return 0
639
+ if isinstance(messages, str):
640
+ msgs = messages
641
+ else:
642
+ msgs = json.dumps(messages)
643
+ try:
644
+ return len(self.tokenizer(msgs))
645
+ except Exception as err:
646
+ print(f"Unable to count tokens with tokenizer: {err}")
647
+ return 0
648
+
649
+ def token_count_for_image(self, fname):
650
+ """
651
+ Calculate the token cost for an image assuming high detail.
652
+ The token cost is determined by the size of the image.
653
+ :param fname: The filename of the image.
654
+ :return: The token cost for the image.
655
+ """
656
+ width, height = self.get_image_size(fname)
657
+ max_dimension = max(width, height)
658
+ if max_dimension > 2048:
659
+ scale_factor = 2048 / max_dimension
660
+ width = int(width * scale_factor)
661
+ height = int(height * scale_factor)
662
+ min_dimension = min(width, height)
663
+ scale_factor = 768 / min_dimension
664
+ width = int(width * scale_factor)
665
+ height = int(height * scale_factor)
666
+ tiles_width = math.ceil(width / 512)
667
+ tiles_height = math.ceil(height / 512)
668
+ num_tiles = tiles_width * tiles_height
669
+ token_cost = num_tiles * 170 + 85
670
+ return token_cost
671
+
672
+ def get_image_size(self, fname):
673
+ """
674
+ Retrieve the size of an image.
675
+ :param fname: The filename of the image.
676
+ :return: A tuple (width, height) representing the image size in pixels.
677
+ """
678
+ with Image.open(fname) as img:
679
+ return img.size
680
+
681
+ def fast_validate_environment(self):
682
+ """Fast path for common models. Avoids forcing litellm import."""
683
+ model = self.name
684
+ pieces = model.split("/")
685
+ if len(pieces) > 1:
686
+ provider = pieces[0]
687
+ else:
688
+ provider = None
689
+ keymap = dict(
690
+ openrouter="OPENROUTER_API_KEY",
691
+ openai="OPENAI_API_KEY",
692
+ deepseek="DEEPSEEK_API_KEY",
693
+ gemini="GEMINI_API_KEY",
694
+ anthropic="ANTHROPIC_API_KEY",
695
+ groq="GROQ_API_KEY",
696
+ fireworks_ai="FIREWORKS_API_KEY",
697
+ )
698
+ var = None
699
+ if model in OPENAI_MODELS:
700
+ var = "OPENAI_API_KEY"
701
+ elif model in ANTHROPIC_MODELS:
702
+ var = "ANTHROPIC_API_KEY"
703
+ else:
704
+ var = keymap.get(provider)
705
+ if var and os.environ.get(var):
706
+ return dict(keys_in_environment=[var], missing_keys=[])
707
+ if not var and provider and model_info_manager.provider_manager.supports_provider(provider):
708
+ provider_keys = model_info_manager.provider_manager.get_required_api_keys(provider)
709
+ for env_var in provider_keys:
710
+ if os.environ.get(env_var):
711
+ return dict(keys_in_environment=[env_var], missing_keys=[])
712
+
713
+ def validate_environment(self):
714
+ res = self.fast_validate_environment()
715
+ if res:
716
+ return res
717
+ model = self.name
718
+ res = litellm.validate_environment(model)
719
+ if res["missing_keys"] and any(
720
+ key in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] for key in res["missing_keys"]
721
+ ):
722
+ if model.startswith("bedrock/") or model.startswith("us.anthropic."):
723
+ if os.environ.get("AWS_PROFILE"):
724
+ res["missing_keys"] = [
725
+ k
726
+ for k in res["missing_keys"]
727
+ if k not in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
728
+ ]
729
+ if not res["missing_keys"]:
730
+ res["keys_in_environment"] = True
731
+ if res["keys_in_environment"]:
732
+ return res
733
+ if res["missing_keys"]:
734
+ return res
735
+ provider = self.info.get("litellm_provider", "").lower()
736
+ provider_config = model_info_manager.provider_manager.get_provider_config(provider)
737
+ if provider_config:
738
+ envs = provider_config.get("api_key_env", [])
739
+ available = [env for env in envs if os.environ.get(env)]
740
+ if available:
741
+ return dict(keys_in_environment=available, missing_keys=[])
742
+ if envs:
743
+ return dict(keys_in_environment=False, missing_keys=envs)
744
+ if provider == "cohere_chat":
745
+ return validate_variables(["COHERE_API_KEY"])
746
+ if provider == "gemini":
747
+ return validate_variables(["GEMINI_API_KEY"])
748
+ if provider == "groq":
749
+ return validate_variables(["GROQ_API_KEY"])
750
+ return res
751
+
752
+ def get_repo_map_tokens(self):
753
+ map_tokens = 1024
754
+ max_inp_tokens = self.info.get("max_input_tokens")
755
+ if max_inp_tokens:
756
+ map_tokens = max_inp_tokens / 8
757
+ map_tokens = min(map_tokens, 4096)
758
+ map_tokens = max(map_tokens, 1024)
759
+ return map_tokens
760
+
761
+ def set_reasoning_effort(self, effort):
762
+ """Set the reasoning effort parameter for models that support it"""
763
+ if effort is not None:
764
+ if self.name.startswith("openrouter/"):
765
+ if not self.extra_params:
766
+ self.extra_params = {}
767
+ if "extra_body" not in self.extra_params:
768
+ self.extra_params["extra_body"] = {}
769
+ self.extra_params["extra_body"]["reasoning"] = {"effort": effort}
770
+ else:
771
+ if not self.extra_params:
772
+ self.extra_params = {}
773
+ if "extra_body" not in self.extra_params:
774
+ self.extra_params["extra_body"] = {}
775
+ self.extra_params["extra_body"]["reasoning_effort"] = effort
776
+
777
+ def parse_token_value(self, value):
778
+ """
779
+ Parse a token value string into an integer.
780
+ Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
781
+
782
+ Args:
783
+ value: String or int token value
784
+
785
+ Returns:
786
+ Integer token value
787
+ """
788
+ if isinstance(value, int):
789
+ return value
790
+ if not isinstance(value, str):
791
+ return int(value)
792
+ value = value.strip().upper()
793
+ if value.endswith("K"):
794
+ multiplier = 1024
795
+ value = value[:-1]
796
+ elif value.endswith("M"):
797
+ multiplier = 1024 * 1024
798
+ value = value[:-1]
799
+ else:
800
+ multiplier = 1
801
+ return int(float(value) * multiplier)
802
+
803
+ def set_thinking_tokens(self, value):
804
+ """
805
+ Set the thinking token budget for models that support it.
806
+ Accepts formats: 8096, "8k", "10.5k", "0.5M", "10K", etc.
807
+ Pass "0" to disable thinking tokens.
808
+ """
809
+ if value is not None:
810
+ num_tokens = self.parse_token_value(value)
811
+ self.use_temperature = False
812
+ if not self.extra_params:
813
+ self.extra_params = {}
814
+ if self.name.startswith("openrouter/"):
815
+ if "extra_body" not in self.extra_params:
816
+ self.extra_params["extra_body"] = {}
817
+ if num_tokens > 0:
818
+ self.extra_params["extra_body"]["reasoning"] = {"max_tokens": num_tokens}
819
+ elif "reasoning" in self.extra_params["extra_body"]:
820
+ del self.extra_params["extra_body"]["reasoning"]
821
+ elif num_tokens > 0:
822
+ self.extra_params["thinking"] = {"type": "enabled", "budget_tokens": num_tokens}
823
+ elif "thinking" in self.extra_params:
824
+ del self.extra_params["thinking"]
825
+
826
+ def get_raw_thinking_tokens(self):
827
+ """Get formatted thinking token budget if available"""
828
+ budget = None
829
+ if self.extra_params:
830
+ if self.name.startswith("openrouter/"):
831
+ if (
832
+ "extra_body" in self.extra_params
833
+ and "reasoning" in self.extra_params["extra_body"]
834
+ and "max_tokens" in self.extra_params["extra_body"]["reasoning"]
835
+ ):
836
+ budget = self.extra_params["extra_body"]["reasoning"]["max_tokens"]
837
+ elif (
838
+ "thinking" in self.extra_params and "budget_tokens" in self.extra_params["thinking"]
839
+ ):
840
+ budget = self.extra_params["thinking"]["budget_tokens"]
841
+ return budget
842
+
843
+ def get_thinking_tokens(self):
844
+ budget = self.get_raw_thinking_tokens()
845
+ if budget is not None:
846
+ if budget >= 1024 * 1024:
847
+ value = budget / (1024 * 1024)
848
+ if value == int(value):
849
+ return f"{int(value)}M"
850
+ else:
851
+ return f"{value:.1f}M"
852
+ else:
853
+ value = budget / 1024
854
+ if value == int(value):
855
+ return f"{int(value)}k"
856
+ else:
857
+ return f"{value:.1f}k"
858
+ return None
859
+
860
+ def get_reasoning_effort(self):
861
+ """Get reasoning effort value if available"""
862
+ if self.extra_params:
863
+ if self.name.startswith("openrouter/"):
864
+ if (
865
+ "extra_body" in self.extra_params
866
+ and "reasoning" in self.extra_params["extra_body"]
867
+ and "effort" in self.extra_params["extra_body"]["reasoning"]
868
+ ):
869
+ return self.extra_params["extra_body"]["reasoning"]["effort"]
870
+ elif (
871
+ "extra_body" in self.extra_params
872
+ and "reasoning_effort" in self.extra_params["extra_body"]
873
+ ):
874
+ return self.extra_params["extra_body"]["reasoning_effort"]
875
+ return None
876
+
877
+ def is_deepseek(self):
878
+ name = self.name.lower()
879
+ if "deepseek" not in name:
880
+ return
881
+ return True
882
+
883
+ def is_anthropic(self):
884
+ name = self.name.lower()
885
+ if "claude" not in name:
886
+ return
887
+ return True
888
+
889
+ def is_ollama(self):
890
+ return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/")
891
+
892
+ async def send_completion(
893
+ self, messages, functions, stream, temperature=None, tools=None, max_tokens=None
894
+ ):
895
+ if os.environ.get("CECLI_SANITY_CHECK_TURNS"):
896
+ sanity_check_messages(messages)
897
+ messages = model_request_parser(self, messages)
898
+ if self.verbose:
899
+ for message in messages:
900
+ msg_role = message.get("role")
901
+ msg_content = message.get("content") if message.get("content") else ""
902
+ msg_trunc = ""
903
+ if message.get("content"):
904
+ msg_trunc = message.get("content")[:30]
905
+ print(f"{msg_role} ({len(msg_content)}): {msg_trunc}")
906
+ kwargs = dict(model=self.name, stream=stream)
907
+ if self.use_temperature is not False:
908
+ if temperature is None:
909
+ if isinstance(self.use_temperature, bool):
910
+ temperature = 0
911
+ else:
912
+ temperature = float(self.use_temperature)
913
+ kwargs["temperature"] = temperature
914
+ effective_tools = tools
915
+ if effective_tools is None and functions:
916
+ effective_tools = [dict(type="function", function=f) for f in functions]
917
+ if effective_tools:
918
+ kwargs["tools"] = effective_tools
919
+ if functions and len(functions) == 1:
920
+ function = functions[0]
921
+ if "name" in function:
922
+ tool_name = function.get("name")
923
+ if tool_name:
924
+ kwargs["tool_choice"] = {"type": "function", "function": {"name": tool_name}}
925
+ if self.extra_params:
926
+ kwargs.update(self.extra_params)
927
+ if max_tokens:
928
+ kwargs["max_tokens"] = max_tokens
929
+ if "max_tokens" in kwargs and kwargs["max_tokens"]:
930
+ kwargs["max_completion_tokens"] = kwargs.pop("max_tokens")
931
+ if self.is_ollama() and "num_ctx" not in kwargs:
932
+ num_ctx = int(self.token_count(messages) * 1.25) + 8192
933
+ kwargs["num_ctx"] = num_ctx
934
+ key = json.dumps(kwargs, sort_keys=True).encode()
935
+ hash_object = hashlib.sha1(key)
936
+ if "timeout" not in kwargs:
937
+ kwargs["timeout"] = request_timeout
938
+ if self.verbose:
939
+ dump(kwargs)
940
+ kwargs["messages"] = messages
941
+ if not self.is_anthropic():
942
+ kwargs["cache_control_injection_points"] = [
943
+ {"location": "message", "role": "system"},
944
+ {"location": "message", "index": -1},
945
+ {"location": "message", "index": -2},
946
+ ]
947
+ if "GITHUB_COPILOT_TOKEN" in os.environ or self.name.startswith("github_copilot/"):
948
+ if "extra_headers" not in kwargs:
949
+ kwargs["extra_headers"] = {
950
+ "Editor-Version": f"cecli/{__version__}",
951
+ "Copilot-Integration-Id": "vscode-chat",
952
+ }
953
+ try:
954
+ res = await litellm.acompletion(**kwargs)
955
+ except Exception as err:
956
+ print(f"LiteLLM API Error: {str(err)}")
957
+ res = self.model_error_response()
958
+ if self.verbose:
959
+ print(f"LiteLLM API Error: {str(err)}")
960
+ raise
961
+ return hash_object, res
962
+
963
+ async def simple_send_with_retries(self, messages, max_tokens=None):
964
+ from cecli.exceptions import LiteLLMExceptions
965
+
966
+ litellm_ex = LiteLLMExceptions()
967
+ messages = model_request_parser(self, messages)
968
+ retry_delay = 0.125
969
+ if self.verbose:
970
+ dump(messages)
971
+ while True:
972
+ try:
973
+ _hash, response = await self.send_completion(
974
+ messages=messages, functions=None, stream=False, max_tokens=max_tokens
975
+ )
976
+ if not response or not hasattr(response, "choices") or not response.choices:
977
+ return None
978
+ res = response.choices[0].message.content
979
+ from cecli.reasoning_tags import remove_reasoning_content
980
+
981
+ return remove_reasoning_content(res, self.reasoning_tag)
982
+ except litellm_ex.exceptions_tuple() as err:
983
+ ex_info = litellm_ex.get_ex_info(err)
984
+ print(str(err))
985
+ if ex_info.description:
986
+ print(ex_info.description)
987
+ should_retry = ex_info.retry
988
+ if should_retry:
989
+ retry_delay *= 2
990
+ if retry_delay > RETRY_TIMEOUT:
991
+ should_retry = False
992
+ if not should_retry:
993
+ return None
994
+ print(f"Retrying in {retry_delay:.1f} seconds...")
995
+ time.sleep(retry_delay)
996
+ continue
997
+ except AttributeError:
998
+ return None
999
+
1000
+ async def model_error_response(self):
1001
+ for i in range(1):
1002
+ await asyncio.sleep(0.1)
1003
+ yield litellm.ModelResponse(
1004
+ choices=[
1005
+ litellm.Choices(
1006
+ finish_reason="stop",
1007
+ index=0,
1008
+ message=litellm.Message(
1009
+ content="Model API Response Error. Please retry the previous request"
1010
+ ),
1011
+ )
1012
+ ],
1013
+ model=self.name,
1014
+ )
1015
+
1016
+
1017
+ def register_models(model_settings_fnames):
1018
+ files_loaded = []
1019
+ for model_settings_fname in model_settings_fnames:
1020
+ if not os.path.exists(model_settings_fname):
1021
+ continue
1022
+ if not Path(model_settings_fname).read_text().strip():
1023
+ continue
1024
+ try:
1025
+ with open(model_settings_fname, "r") as model_settings_file:
1026
+ model_settings_list = yaml.safe_load(model_settings_file)
1027
+ for model_settings_dict in model_settings_list:
1028
+ model_settings = ModelSettings(**model_settings_dict)
1029
+ MODEL_SETTINGS[:] = [ms for ms in MODEL_SETTINGS if ms.name != model_settings.name]
1030
+ MODEL_SETTINGS.append(model_settings)
1031
+ except Exception as e:
1032
+ raise Exception(f"Error loading model settings from {model_settings_fname}: {e}")
1033
+ files_loaded.append(model_settings_fname)
1034
+ return files_loaded
1035
+
1036
+
1037
+ def register_litellm_models(model_fnames):
1038
+ files_loaded = []
1039
+ for model_fname in model_fnames:
1040
+ if not os.path.exists(model_fname):
1041
+ continue
1042
+ try:
1043
+ data = Path(model_fname).read_text()
1044
+ if not data.strip():
1045
+ continue
1046
+ model_def = json.loads(data)
1047
+ if not model_def:
1048
+ continue
1049
+ model_info_manager.local_model_metadata.update(model_def)
1050
+ except Exception as e:
1051
+ raise Exception(f"Error loading model definition from {model_fname}: {e}")
1052
+ files_loaded.append(model_fname)
1053
+ return files_loaded
1054
+
1055
+
1056
+ def validate_variables(vars):
1057
+ missing = []
1058
+ for var in vars:
1059
+ if var not in os.environ:
1060
+ missing.append(var)
1061
+ if missing:
1062
+ return dict(keys_in_environment=False, missing_keys=missing)
1063
+ return dict(keys_in_environment=True, missing_keys=missing)
1064
+
1065
+
1066
+ async def sanity_check_models(io, main_model):
1067
+ problem_main = await sanity_check_model(io, main_model)
1068
+ problem_weak = None
1069
+ if main_model.weak_model and main_model.weak_model is not main_model:
1070
+ problem_weak = await sanity_check_model(io, main_model.weak_model)
1071
+ problem_editor = None
1072
+ if (
1073
+ main_model.editor_model
1074
+ and main_model.editor_model is not main_model
1075
+ and main_model.editor_model is not main_model.weak_model
1076
+ ):
1077
+ problem_editor = await sanity_check_model(io, main_model.editor_model)
1078
+ return problem_main or problem_weak or problem_editor
1079
+
1080
+
1081
+ async def sanity_check_model(io, model):
1082
+ if getattr(model, "copy_paste_transport", "api") == "clipboard":
1083
+ return False
1084
+ show = False
1085
+ if model.missing_keys:
1086
+ show = True
1087
+ io.tool_warning(f"Warning: {model} expects these environment variables")
1088
+ for key in model.missing_keys:
1089
+ value = os.environ.get(key, "")
1090
+ status = "Set" if value else "Not set"
1091
+ io.tool_output(f"- {key}: {status}")
1092
+ if platform.system() == "Windows":
1093
+ io.tool_output(
1094
+ "Note: You may need to restart your terminal or command prompt for `setx` to take"
1095
+ " effect."
1096
+ )
1097
+ elif not model.keys_in_environment:
1098
+ show = True
1099
+ io.tool_warning(f"Warning for {model}: Unknown which environment variables are required.")
1100
+ await check_for_dependencies(io, model.name)
1101
+ if not model.info:
1102
+ show = True
1103
+ io.tool_warning(
1104
+ f"Warning for {model}: Unknown context window size and costs, using sane defaults."
1105
+ )
1106
+ possible_matches = fuzzy_match_models(model.name)
1107
+ if possible_matches:
1108
+ io.tool_output("Did you mean one of these?")
1109
+ for match in possible_matches:
1110
+ io.tool_output(f"- {match}")
1111
+ return show
1112
+
1113
+
1114
+ async def check_for_dependencies(io, model_name):
1115
+ """
1116
+ Check for model-specific dependencies and install them if needed.
1117
+
1118
+ Args:
1119
+ io: The IO object for user interaction
1120
+ model_name: The name of the model to check dependencies for
1121
+ """
1122
+ if model_name.startswith("bedrock/"):
1123
+ await check_pip_install_extra(
1124
+ io, "boto3", "AWS Bedrock models require the boto3 package.", ["boto3"]
1125
+ )
1126
+ elif model_name.startswith("vertex_ai/"):
1127
+ await check_pip_install_extra(
1128
+ io,
1129
+ "google.cloud.aiplatform",
1130
+ "Google Vertex AI models require the google-cloud-aiplatform package.",
1131
+ ["google-cloud-aiplatform"],
1132
+ )
1133
+
1134
+
1135
+ def get_chat_model_names():
1136
+ chat_models = set()
1137
+ model_metadata = list(litellm.model_cost.items())
1138
+ model_metadata += list(model_info_manager.local_model_metadata.items())
1139
+ openai_provider_models = model_info_manager.provider_manager.get_models_for_listing()
1140
+ model_metadata += list(openai_provider_models.items())
1141
+ for orig_model, attrs in model_metadata:
1142
+ if attrs.get("mode") != "chat":
1143
+ continue
1144
+ provider = (attrs.get("litellm_provider") or "").lower()
1145
+ if provider:
1146
+ prefix = provider + "/"
1147
+ if orig_model.lower().startswith(prefix):
1148
+ fq_model = orig_model
1149
+ else:
1150
+ fq_model = f"{provider}/{orig_model}"
1151
+ chat_models.add(fq_model)
1152
+ chat_models.add(orig_model)
1153
+ return sorted(chat_models)
1154
+
1155
+
1156
+ def fuzzy_match_models(name):
1157
+ name = name.lower()
1158
+ chat_models = get_chat_model_names()
1159
+ matching_models = [m for m in chat_models if name in m.lower()]
1160
+ if matching_models:
1161
+ return sorted(set(matching_models))
1162
+ models = set(chat_models)
1163
+ matching_models = difflib.get_close_matches(name, models, n=3, cutoff=0.8)
1164
+ return sorted(set(matching_models))
1165
+
1166
+
1167
+ def print_matching_models(io, search):
1168
+ matches = fuzzy_match_models(search)
1169
+ if matches:
1170
+ io.tool_output(f'Models which match "{search}":')
1171
+ for model in matches:
1172
+ # Get model info to check for prices
1173
+ info = model_info_manager.get_model_info(model)
1174
+
1175
+ # Build price string
1176
+ price_parts = []
1177
+
1178
+ # Check for input cost
1179
+ input_cost = info.get("input_cost_per_token")
1180
+ if input_cost is not None:
1181
+ # Convert from per-token to per-1M tokens
1182
+ input_cost_per_1m = input_cost * 1000000
1183
+ price_parts.append(f"${input_cost_per_1m:.2f}/1m/input")
1184
+
1185
+ # Check for output cost
1186
+ output_cost = info.get("output_cost_per_token")
1187
+ if output_cost is not None:
1188
+ # Convert from per-token to per-1M tokens
1189
+ output_cost_per_1m = output_cost * 1000000
1190
+ price_parts.append(f"${output_cost_per_1m:.2f}/1m/output")
1191
+
1192
+ # Check for cache cost (if available)
1193
+ cache_cost = info.get("cache_cost_per_token")
1194
+ if cache_cost is not None:
1195
+ # Convert from per-token to per-1M tokens
1196
+ cache_cost_per_1m = cache_cost * 1000000
1197
+ price_parts.append(f"${cache_cost_per_1m:.2f}/1m/cache")
1198
+
1199
+ # Format the output
1200
+ if price_parts:
1201
+ price_str = " (" + ", ".join(price_parts) + ")"
1202
+ io.tool_output(f"- {model}{price_str}")
1203
+ else:
1204
+ io.tool_output(f"- {model}")
1205
+ else:
1206
+ io.tool_output(f'No models match "{search}".')
1207
+
1208
+
1209
+ def get_model_settings_as_yaml():
1210
+ from dataclasses import fields
1211
+
1212
+ import yaml
1213
+
1214
+ model_settings_list = []
1215
+ defaults = {}
1216
+ for field in fields(ModelSettings):
1217
+ defaults[field.name] = field.default
1218
+ defaults["name"] = "(default values)"
1219
+ model_settings_list.append(defaults)
1220
+ for ms in sorted(MODEL_SETTINGS, key=lambda x: x.name):
1221
+ model_settings_dict = {}
1222
+ for field in fields(ModelSettings):
1223
+ value = getattr(ms, field.name)
1224
+ if value != field.default:
1225
+ model_settings_dict[field.name] = value
1226
+ model_settings_list.append(model_settings_dict)
1227
+ model_settings_list.append(None)
1228
+ yaml_str = yaml.dump(
1229
+ [ms for ms in model_settings_list if ms is not None],
1230
+ default_flow_style=False,
1231
+ sort_keys=False,
1232
+ )
1233
+ return yaml_str.replace("\n- ", "\n\n- ")
1234
+
1235
+
1236
+ def main():
1237
+ if len(sys.argv) < 2:
1238
+ print("Usage: python models.py <model_name> or python models.py --yaml")
1239
+ sys.exit(1)
1240
+ if sys.argv[1] == "--yaml":
1241
+ yaml_string = get_model_settings_as_yaml()
1242
+ print(yaml_string)
1243
+ else:
1244
+ model_name = sys.argv[1]
1245
+ matching_models = fuzzy_match_models(model_name)
1246
+ if matching_models:
1247
+ print(f"Matching models for '{model_name}':")
1248
+ for model in matching_models:
1249
+ print(model)
1250
+ else:
1251
+ print(f"No matching models found for '{model_name}'.")
1252
+
1253
+
1254
+ if __name__ == "__main__":
1255
+ main()