patch-code 0.1.0__py3-none-any.whl

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