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.
- cecli/__init__.py +20 -0
- cecli/__main__.py +4 -0
- cecli/_version.py +34 -0
- cecli/args.py +1092 -0
- cecli/args_formatter.py +228 -0
- cecli/change_tracker.py +133 -0
- cecli/coders/__init__.py +38 -0
- cecli/coders/agent_coder.py +1872 -0
- cecli/coders/architect_coder.py +63 -0
- cecli/coders/ask_coder.py +8 -0
- cecli/coders/base_coder.py +3993 -0
- cecli/coders/chat_chunks.py +116 -0
- cecli/coders/context_coder.py +52 -0
- cecli/coders/copypaste_coder.py +269 -0
- cecli/coders/editblock_coder.py +656 -0
- cecli/coders/editblock_fenced_coder.py +9 -0
- cecli/coders/editblock_func_coder.py +140 -0
- cecli/coders/editor_diff_fenced_coder.py +8 -0
- cecli/coders/editor_editblock_coder.py +8 -0
- cecli/coders/editor_whole_coder.py +8 -0
- cecli/coders/help_coder.py +15 -0
- cecli/coders/patch_coder.py +705 -0
- cecli/coders/search_replace.py +757 -0
- cecli/coders/shell.py +37 -0
- cecli/coders/single_wholefile_func_coder.py +101 -0
- cecli/coders/udiff_coder.py +428 -0
- cecli/coders/udiff_simple.py +12 -0
- cecli/coders/wholefile_coder.py +143 -0
- cecli/coders/wholefile_func_coder.py +133 -0
- cecli/commands/__init__.py +192 -0
- cecli/commands/add.py +226 -0
- cecli/commands/agent.py +51 -0
- cecli/commands/architect.py +46 -0
- cecli/commands/ask.py +44 -0
- cecli/commands/chat_mode.py +0 -0
- cecli/commands/clear.py +37 -0
- cecli/commands/code.py +46 -0
- cecli/commands/command_prefix.py +44 -0
- cecli/commands/commit.py +52 -0
- cecli/commands/context.py +47 -0
- cecli/commands/context_blocks.py +124 -0
- cecli/commands/context_management.py +51 -0
- cecli/commands/copy.py +62 -0
- cecli/commands/copy_context.py +81 -0
- cecli/commands/core.py +287 -0
- cecli/commands/diff.py +68 -0
- cecli/commands/drop.py +217 -0
- cecli/commands/editor.py +78 -0
- cecli/commands/exit.py +55 -0
- cecli/commands/git.py +57 -0
- cecli/commands/help.py +140 -0
- cecli/commands/history_search.py +40 -0
- cecli/commands/lint.py +109 -0
- cecli/commands/list_sessions.py +56 -0
- cecli/commands/load.py +85 -0
- cecli/commands/load_session.py +48 -0
- cecli/commands/load_skill.py +68 -0
- cecli/commands/ls.py +75 -0
- cecli/commands/map.py +37 -0
- cecli/commands/map_refresh.py +35 -0
- cecli/commands/model.py +118 -0
- cecli/commands/models.py +41 -0
- cecli/commands/multiline_mode.py +38 -0
- cecli/commands/paste.py +91 -0
- cecli/commands/quit.py +32 -0
- cecli/commands/read_only.py +267 -0
- cecli/commands/read_only_stub.py +270 -0
- cecli/commands/reasoning_effort.py +70 -0
- cecli/commands/remove_skill.py +68 -0
- cecli/commands/report.py +40 -0
- cecli/commands/reset.py +88 -0
- cecli/commands/run.py +99 -0
- cecli/commands/save.py +49 -0
- cecli/commands/save_session.py +43 -0
- cecli/commands/settings.py +69 -0
- cecli/commands/test.py +58 -0
- cecli/commands/think_tokens.py +74 -0
- cecli/commands/tokens.py +207 -0
- cecli/commands/undo.py +145 -0
- cecli/commands/utils/__init__.py +0 -0
- cecli/commands/utils/base_command.py +131 -0
- cecli/commands/utils/helpers.py +142 -0
- cecli/commands/utils/registry.py +53 -0
- cecli/commands/utils/save_load_manager.py +98 -0
- cecli/commands/voice.py +78 -0
- cecli/commands/weak_model.py +123 -0
- cecli/commands/web.py +87 -0
- cecli/deprecated_args.py +185 -0
- cecli/diffs.py +129 -0
- cecli/dump.py +29 -0
- cecli/editor.py +147 -0
- cecli/exceptions.py +115 -0
- cecli/format_settings.py +26 -0
- cecli/help.py +119 -0
- cecli/help_pats.py +19 -0
- cecli/helpers/__init__.py +9 -0
- cecli/helpers/copypaste.py +123 -0
- cecli/helpers/coroutines.py +8 -0
- cecli/helpers/file_searcher.py +142 -0
- cecli/helpers/model_providers.py +552 -0
- cecli/helpers/plugin_manager.py +81 -0
- cecli/helpers/profiler.py +162 -0
- cecli/helpers/requests.py +77 -0
- cecli/helpers/similarity.py +98 -0
- cecli/helpers/skills.py +577 -0
- cecli/history.py +186 -0
- cecli/io.py +1782 -0
- cecli/linter.py +304 -0
- cecli/llm.py +101 -0
- cecli/main.py +1280 -0
- cecli/mcp/__init__.py +154 -0
- cecli/mcp/oauth.py +250 -0
- cecli/mcp/server.py +278 -0
- cecli/mdstream.py +243 -0
- cecli/models.py +1255 -0
- cecli/onboarding.py +301 -0
- cecli/prompts/__init__.py +0 -0
- cecli/prompts/agent.yml +71 -0
- cecli/prompts/architect.yml +35 -0
- cecli/prompts/ask.yml +31 -0
- cecli/prompts/base.yml +99 -0
- cecli/prompts/context.yml +60 -0
- cecli/prompts/copypaste.yml +5 -0
- cecli/prompts/editblock.yml +143 -0
- cecli/prompts/editblock_fenced.yml +106 -0
- cecli/prompts/editblock_func.yml +25 -0
- cecli/prompts/editor_diff_fenced.yml +115 -0
- cecli/prompts/editor_editblock.yml +121 -0
- cecli/prompts/editor_whole.yml +46 -0
- cecli/prompts/help.yml +37 -0
- cecli/prompts/patch.yml +110 -0
- cecli/prompts/single_wholefile_func.yml +24 -0
- cecli/prompts/udiff.yml +106 -0
- cecli/prompts/udiff_simple.yml +13 -0
- cecli/prompts/utils/__init__.py +0 -0
- cecli/prompts/utils/prompt_registry.py +167 -0
- cecli/prompts/utils/system.py +56 -0
- cecli/prompts/wholefile.yml +50 -0
- cecli/prompts/wholefile_func.yml +24 -0
- cecli/queries/tree-sitter-language-pack/README.md +7 -0
- cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
- cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
- cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
- cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
- cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
- cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
- cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
- cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
- cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- cecli/queries/tree-sitter-languages/README.md +24 -0
- cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
- cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
- cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
- cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
- cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
- cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
- cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
- cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
- cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
- cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
- cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
- cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
- cecli/reasoning_tags.py +82 -0
- cecli/repo.py +626 -0
- cecli/repomap.py +1368 -0
- cecli/report.py +260 -0
- cecli/resources/__init__.py +3 -0
- cecli/resources/model-metadata.json +25751 -0
- cecli/resources/model-settings.yml +2394 -0
- cecli/resources/providers.json +67 -0
- cecli/run_cmd.py +143 -0
- cecli/scrape.py +295 -0
- cecli/sendchat.py +250 -0
- cecli/sessions.py +281 -0
- cecli/special.py +203 -0
- cecli/tools/__init__.py +72 -0
- cecli/tools/command.py +103 -0
- cecli/tools/command_interactive.py +113 -0
- cecli/tools/context_manager.py +175 -0
- cecli/tools/delete_block.py +154 -0
- cecli/tools/delete_line.py +120 -0
- cecli/tools/delete_lines.py +144 -0
- cecli/tools/extract_lines.py +281 -0
- cecli/tools/finished.py +35 -0
- cecli/tools/git_branch.py +132 -0
- cecli/tools/git_diff.py +49 -0
- cecli/tools/git_log.py +43 -0
- cecli/tools/git_remote.py +39 -0
- cecli/tools/git_show.py +37 -0
- cecli/tools/git_status.py +32 -0
- cecli/tools/grep.py +242 -0
- cecli/tools/indent_lines.py +195 -0
- cecli/tools/insert_block.py +263 -0
- cecli/tools/list_changes.py +71 -0
- cecli/tools/load_skill.py +51 -0
- cecli/tools/ls.py +77 -0
- cecli/tools/remove_skill.py +51 -0
- cecli/tools/replace_all.py +113 -0
- cecli/tools/replace_line.py +135 -0
- cecli/tools/replace_lines.py +180 -0
- cecli/tools/replace_text.py +186 -0
- cecli/tools/show_numbered_context.py +137 -0
- cecli/tools/thinking.py +52 -0
- cecli/tools/undo_change.py +82 -0
- cecli/tools/update_todo_list.py +148 -0
- cecli/tools/utils/base_tool.py +64 -0
- cecli/tools/utils/helpers.py +359 -0
- cecli/tools/utils/output.py +119 -0
- cecli/tools/utils/registry.py +145 -0
- cecli/tools/view_files_matching.py +138 -0
- cecli/tools/view_files_with_symbol.py +117 -0
- cecli/tui/__init__.py +83 -0
- cecli/tui/app.py +971 -0
- cecli/tui/io.py +566 -0
- cecli/tui/styles.tcss +117 -0
- cecli/tui/widgets/__init__.py +19 -0
- cecli/tui/widgets/completion_bar.py +331 -0
- cecli/tui/widgets/file_list.py +76 -0
- cecli/tui/widgets/footer.py +165 -0
- cecli/tui/widgets/input_area.py +320 -0
- cecli/tui/widgets/key_hints.py +16 -0
- cecli/tui/widgets/output.py +354 -0
- cecli/tui/widgets/status_bar.py +279 -0
- cecli/tui/worker.py +160 -0
- cecli/urls.py +16 -0
- cecli/utils.py +499 -0
- cecli/versioncheck.py +90 -0
- cecli/voice.py +90 -0
- cecli/waiting.py +38 -0
- cecli/watch.py +316 -0
- cecli/watch_prompts.py +12 -0
- cecli/website/Gemfile +8 -0
- cecli/website/_includes/blame.md +162 -0
- cecli/website/_includes/get-started.md +22 -0
- cecli/website/_includes/help-tip.md +5 -0
- cecli/website/_includes/help.md +24 -0
- cecli/website/_includes/install.md +5 -0
- cecli/website/_includes/keys.md +4 -0
- cecli/website/_includes/model-warnings.md +67 -0
- cecli/website/_includes/multi-line.md +22 -0
- cecli/website/_includes/python-m-aider.md +5 -0
- cecli/website/_includes/recording.css +228 -0
- cecli/website/_includes/recording.md +34 -0
- cecli/website/_includes/replit-pipx.md +9 -0
- cecli/website/_includes/works-best.md +1 -0
- cecli/website/_sass/custom/custom.scss +103 -0
- cecli/website/docs/config/adv-model-settings.md +2498 -0
- cecli/website/docs/config/agent-mode.md +320 -0
- cecli/website/docs/config/aider_conf.md +548 -0
- cecli/website/docs/config/api-keys.md +90 -0
- cecli/website/docs/config/custom-commands.md +187 -0
- cecli/website/docs/config/dotenv.md +493 -0
- cecli/website/docs/config/editor.md +127 -0
- cecli/website/docs/config/mcp.md +210 -0
- cecli/website/docs/config/model-aliases.md +173 -0
- cecli/website/docs/config/options.md +890 -0
- cecli/website/docs/config/reasoning.md +210 -0
- cecli/website/docs/config/skills.md +172 -0
- cecli/website/docs/config/tui.md +126 -0
- cecli/website/docs/config.md +44 -0
- cecli/website/docs/faq.md +379 -0
- cecli/website/docs/git.md +76 -0
- cecli/website/docs/index.md +47 -0
- cecli/website/docs/install/codespaces.md +39 -0
- cecli/website/docs/install/docker.md +48 -0
- cecli/website/docs/install/optional.md +100 -0
- cecli/website/docs/install/replit.md +8 -0
- cecli/website/docs/install.md +115 -0
- cecli/website/docs/languages.md +264 -0
- cecli/website/docs/legal/contributor-agreement.md +111 -0
- cecli/website/docs/legal/privacy.md +104 -0
- cecli/website/docs/llms/anthropic.md +77 -0
- cecli/website/docs/llms/azure.md +48 -0
- cecli/website/docs/llms/bedrock.md +132 -0
- cecli/website/docs/llms/cohere.md +34 -0
- cecli/website/docs/llms/deepseek.md +32 -0
- cecli/website/docs/llms/gemini.md +49 -0
- cecli/website/docs/llms/github.md +111 -0
- cecli/website/docs/llms/groq.md +36 -0
- cecli/website/docs/llms/lm-studio.md +39 -0
- cecli/website/docs/llms/ollama.md +75 -0
- cecli/website/docs/llms/openai-compat.md +39 -0
- cecli/website/docs/llms/openai.md +58 -0
- cecli/website/docs/llms/openrouter.md +78 -0
- cecli/website/docs/llms/other.md +117 -0
- cecli/website/docs/llms/vertex.md +50 -0
- cecli/website/docs/llms/warnings.md +10 -0
- cecli/website/docs/llms/xai.md +53 -0
- cecli/website/docs/llms.md +54 -0
- cecli/website/docs/more/analytics.md +127 -0
- cecli/website/docs/more/edit-formats.md +116 -0
- cecli/website/docs/more/infinite-output.md +192 -0
- cecli/website/docs/more-info.md +8 -0
- cecli/website/docs/recordings/auto-accept-architect.md +31 -0
- cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- cecli/website/docs/recordings/index.md +21 -0
- cecli/website/docs/recordings/model-accepts-settings.md +69 -0
- cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- cecli/website/docs/repomap.md +112 -0
- cecli/website/docs/scripting.md +100 -0
- cecli/website/docs/sessions.md +213 -0
- cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
- cecli/website/docs/troubleshooting/edit-errors.md +76 -0
- cecli/website/docs/troubleshooting/imports.md +62 -0
- cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
- cecli/website/docs/troubleshooting/support.md +79 -0
- cecli/website/docs/troubleshooting/token-limits.md +96 -0
- cecli/website/docs/troubleshooting/warnings.md +12 -0
- cecli/website/docs/troubleshooting.md +11 -0
- cecli/website/docs/usage/browser.md +57 -0
- cecli/website/docs/usage/caching.md +49 -0
- cecli/website/docs/usage/commands.md +133 -0
- cecli/website/docs/usage/conventions.md +119 -0
- cecli/website/docs/usage/copypaste.md +136 -0
- cecli/website/docs/usage/images-urls.md +48 -0
- cecli/website/docs/usage/lint-test.md +118 -0
- cecli/website/docs/usage/modes.md +211 -0
- cecli/website/docs/usage/not-code.md +179 -0
- cecli/website/docs/usage/notifications.md +87 -0
- cecli/website/docs/usage/tips.md +79 -0
- cecli/website/docs/usage/tutorials.md +30 -0
- cecli/website/docs/usage/voice.md +121 -0
- cecli/website/docs/usage/watch.md +294 -0
- cecli/website/docs/usage.md +102 -0
- cecli/website/share/index.md +101 -0
- cecli_dev-0.95.5.dist-info/METADATA +549 -0
- cecli_dev-0.95.5.dist-info/RECORD +366 -0
- cecli_dev-0.95.5.dist-info/WHEEL +5 -0
- cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
- cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
- 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()
|