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.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +34 -0
- aider/analytics.py +258 -0
- aider/args.py +1056 -0
- aider/args_formatter.py +228 -0
- aider/change_tracker.py +133 -0
- aider/coders/__init__.py +36 -0
- aider/coders/agent_coder.py +2166 -0
- aider/coders/agent_prompts.py +104 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +3613 -0
- aider/coders/base_prompts.py +87 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +175 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +9 -0
- aider/coders/editor_editblock_prompts.py +21 -0
- aider/coders/editor_whole_coder.py +9 -0
- aider/coders/editor_whole_prompts.py +12 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +159 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +115 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +65 -0
- aider/commands.py +2173 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +115 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/helpers/__init__.py +9 -0
- aider/helpers/similarity.py +98 -0
- aider/history.py +180 -0
- aider/io.py +1608 -0
- aider/linter.py +304 -0
- aider/llm.py +55 -0
- aider/main.py +1415 -0
- aider/mcp/__init__.py +174 -0
- aider/mcp/server.py +149 -0
- aider/mdstream.py +243 -0
- aider/models.py +1313 -0
- aider/onboarding.py +429 -0
- aider/openrouter.py +129 -0
- aider/prompts.py +56 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +24 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +621 -0
- aider/repomap.py +1174 -0
- aider/report.py +260 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +776 -0
- aider/resources/model-settings.yml +2068 -0
- aider/run_cmd.py +133 -0
- aider/scrape.py +293 -0
- aider/sendchat.py +242 -0
- aider/sessions.py +256 -0
- aider/special.py +203 -0
- aider/tools/__init__.py +72 -0
- aider/tools/command.py +105 -0
- aider/tools/command_interactive.py +122 -0
- aider/tools/delete_block.py +182 -0
- aider/tools/delete_line.py +155 -0
- aider/tools/delete_lines.py +184 -0
- aider/tools/extract_lines.py +341 -0
- aider/tools/finished.py +48 -0
- aider/tools/git_branch.py +129 -0
- aider/tools/git_diff.py +60 -0
- aider/tools/git_log.py +57 -0
- aider/tools/git_remote.py +53 -0
- aider/tools/git_show.py +51 -0
- aider/tools/git_status.py +46 -0
- aider/tools/grep.py +256 -0
- aider/tools/indent_lines.py +221 -0
- aider/tools/insert_block.py +288 -0
- aider/tools/list_changes.py +86 -0
- aider/tools/ls.py +93 -0
- aider/tools/make_editable.py +85 -0
- aider/tools/make_readonly.py +69 -0
- aider/tools/remove.py +91 -0
- aider/tools/replace_all.py +126 -0
- aider/tools/replace_line.py +173 -0
- aider/tools/replace_lines.py +217 -0
- aider/tools/replace_text.py +187 -0
- aider/tools/show_numbered_context.py +147 -0
- aider/tools/tool_utils.py +313 -0
- aider/tools/undo_change.py +95 -0
- aider/tools/update_todo_list.py +156 -0
- aider/tools/view.py +57 -0
- aider/tools/view_files_matching.py +141 -0
- aider/tools/view_files_with_symbol.py +129 -0
- aider/urls.py +17 -0
- aider/utils.py +456 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +205 -0
- aider/waiting.py +38 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +2261 -0
- aider/website/docs/config/agent-mode.md +194 -0
- aider/website/docs/config/aider_conf.md +548 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +493 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/mcp.md +95 -0
- aider/website/docs/config/model-aliases.md +104 -0
- aider/website/docs/config/options.md +890 -0
- aider/website/docs/config/reasoning.md +210 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +384 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +111 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +117 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +127 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +165 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/sessions.md +203 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +133 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +102 -0
- aider/website/share/index.md +101 -0
- aider_ce-0.88.20.dist-info/METADATA +187 -0
- aider_ce-0.88.20.dist-info/RECORD +279 -0
- aider_ce-0.88.20.dist-info/WHEEL +5 -0
- aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
- aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
- 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()
|