superqode 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,472 @@
1
+ """Model download and conversion utilities for HuggingFace models.
2
+
3
+ This module provides utilities for downloading models from HuggingFace Hub
4
+ and preparing them for local use with Ollama or transformers.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import subprocess
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, List, Optional
14
+ from urllib.request import Request, urlopen
15
+
16
+
17
+ @dataclass
18
+ class DownloadProgress:
19
+ """Progress information for model downloads.
20
+
21
+ Attributes:
22
+ filename: File being downloaded
23
+ total_bytes: Total size in bytes
24
+ downloaded_bytes: Bytes downloaded so far
25
+ speed_mbps: Download speed in MB/s
26
+ eta_seconds: Estimated time remaining
27
+ completed: Whether download is complete
28
+ error: Error message if failed
29
+ """
30
+
31
+ filename: str = ""
32
+ total_bytes: int = 0
33
+ downloaded_bytes: int = 0
34
+ speed_mbps: float = 0.0
35
+ eta_seconds: float = 0.0
36
+ completed: bool = False
37
+ error: str = ""
38
+
39
+ @property
40
+ def progress_percent(self) -> float:
41
+ """Get download progress as percentage."""
42
+ if self.total_bytes == 0:
43
+ return 0.0
44
+ return (self.downloaded_bytes / self.total_bytes) * 100
45
+
46
+ @property
47
+ def size_display(self) -> str:
48
+ """Human-readable total size."""
49
+ gb = self.total_bytes / (1024**3)
50
+ if gb >= 1:
51
+ return f"{gb:.1f}GB"
52
+ mb = self.total_bytes / (1024**2)
53
+ return f"{mb:.0f}MB"
54
+
55
+
56
+ @dataclass
57
+ class DownloadResult:
58
+ """Result of a model download.
59
+
60
+ Attributes:
61
+ success: Whether download succeeded
62
+ path: Path to downloaded file(s)
63
+ model_id: HuggingFace model ID
64
+ quantization: Quantization level (for GGUF)
65
+ ollama_model_name: Name to use in Ollama (if applicable)
66
+ error: Error message if failed
67
+ """
68
+
69
+ success: bool
70
+ path: Path = None
71
+ model_id: str = ""
72
+ quantization: str = ""
73
+ ollama_model_name: str = ""
74
+ error: str = ""
75
+
76
+
77
+ class HFDownloader:
78
+ """Download and convert HuggingFace models for local use.
79
+
80
+ Supports:
81
+ - Downloading GGUF files for Ollama/llama.cpp
82
+ - Downloading safetensors for transformers
83
+ - Creating Ollama Modelfiles for downloaded GGUFs
84
+ - Progress tracking with callbacks
85
+
86
+ Environment:
87
+ HF_TOKEN: For private/gated models
88
+ HF_HOME: Cache directory (default: ~/.cache/huggingface)
89
+ """
90
+
91
+ def __init__(self, token: Optional[str] = None, cache_dir: Optional[Path] = None):
92
+ """Initialize the downloader.
93
+
94
+ Args:
95
+ token: HF token for authentication.
96
+ cache_dir: Cache directory for downloads.
97
+ """
98
+ self._token = (
99
+ token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
100
+ )
101
+
102
+ if cache_dir:
103
+ self._cache_dir = cache_dir
104
+ else:
105
+ hf_home = os.environ.get("HF_HOME", os.path.expanduser("~/.cache/huggingface"))
106
+ self._cache_dir = Path(hf_home) / "superqode"
107
+
108
+ # Ensure cache dir exists
109
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ @property
112
+ def cache_dir(self) -> Path:
113
+ """Get the cache directory."""
114
+ return self._cache_dir
115
+
116
+ async def download_gguf(
117
+ self,
118
+ model_id: str,
119
+ quantization: str = "Q4_K_M",
120
+ output_dir: Optional[Path] = None,
121
+ progress_callback: Optional[Callable[[DownloadProgress], None]] = None,
122
+ ) -> DownloadResult:
123
+ """Download a GGUF file from HuggingFace Hub.
124
+
125
+ Args:
126
+ model_id: HuggingFace model ID (e.g., "TheBloke/Llama-2-7B-GGUF")
127
+ quantization: Desired quantization level (Q4_K_M, Q8_0, etc.)
128
+ output_dir: Output directory (defaults to cache)
129
+ progress_callback: Callback for progress updates
130
+
131
+ Returns:
132
+ DownloadResult with download info.
133
+ """
134
+ from superqode.providers.huggingface.hub import get_hf_hub
135
+
136
+ hub = get_hf_hub()
137
+
138
+ # Find GGUF files for this model
139
+ gguf_files = await hub.list_gguf_files(model_id)
140
+
141
+ if not gguf_files:
142
+ return DownloadResult(
143
+ success=False, model_id=model_id, error=f"No GGUF files found for {model_id}"
144
+ )
145
+
146
+ # Find file matching requested quantization
147
+ target_file = None
148
+ quant_upper = quantization.upper()
149
+
150
+ for f in gguf_files:
151
+ if f.quantization.upper() == quant_upper:
152
+ target_file = f
153
+ break
154
+
155
+ # If exact match not found, use first available
156
+ if not target_file:
157
+ target_file = gguf_files[0]
158
+ quantization = target_file.quantization
159
+
160
+ # Set output directory
161
+ out_dir = output_dir or self._cache_dir / "gguf"
162
+ out_dir.mkdir(parents=True, exist_ok=True)
163
+
164
+ output_path = out_dir / target_file.filename
165
+
166
+ # Check if already downloaded
167
+ if output_path.exists() and output_path.stat().st_size == target_file.size_bytes:
168
+ return DownloadResult(
169
+ success=True,
170
+ path=output_path,
171
+ model_id=model_id,
172
+ quantization=quantization,
173
+ )
174
+
175
+ # Download the file
176
+ loop = asyncio.get_event_loop()
177
+
178
+ try:
179
+ await loop.run_in_executor(
180
+ None,
181
+ lambda: self._download_file(
182
+ target_file.url,
183
+ output_path,
184
+ target_file.size_bytes,
185
+ target_file.filename,
186
+ progress_callback,
187
+ ),
188
+ )
189
+
190
+ return DownloadResult(
191
+ success=True,
192
+ path=output_path,
193
+ model_id=model_id,
194
+ quantization=quantization,
195
+ )
196
+
197
+ except Exception as e:
198
+ return DownloadResult(success=False, model_id=model_id, error=str(e))
199
+
200
+ def _download_file(
201
+ self,
202
+ url: str,
203
+ output_path: Path,
204
+ total_size: int,
205
+ filename: str,
206
+ progress_callback: Optional[Callable[[DownloadProgress], None]],
207
+ ) -> None:
208
+ """Download a file with progress tracking."""
209
+ import time
210
+
211
+ headers = {}
212
+ if self._token:
213
+ headers["Authorization"] = f"Bearer {self._token}"
214
+
215
+ request = Request(url, headers=headers)
216
+
217
+ chunk_size = 8192
218
+ downloaded = 0
219
+ start_time = time.time()
220
+ last_update = start_time
221
+
222
+ with urlopen(request, timeout=300) as response:
223
+ with open(output_path, "wb") as f:
224
+ while True:
225
+ chunk = response.read(chunk_size)
226
+ if not chunk:
227
+ break
228
+
229
+ f.write(chunk)
230
+ downloaded += len(chunk)
231
+
232
+ # Update progress every 0.5 seconds
233
+ current_time = time.time()
234
+ if progress_callback and (current_time - last_update) >= 0.5:
235
+ elapsed = current_time - start_time
236
+ speed = (downloaded / (1024**2)) / elapsed if elapsed > 0 else 0
237
+ remaining = total_size - downloaded
238
+ eta = remaining / (downloaded / elapsed) if downloaded > 0 else 0
239
+
240
+ progress_callback(
241
+ DownloadProgress(
242
+ filename=filename,
243
+ total_bytes=total_size,
244
+ downloaded_bytes=downloaded,
245
+ speed_mbps=speed,
246
+ eta_seconds=eta,
247
+ )
248
+ )
249
+ last_update = current_time
250
+
251
+ # Final progress update
252
+ if progress_callback:
253
+ progress_callback(
254
+ DownloadProgress(
255
+ filename=filename,
256
+ total_bytes=total_size,
257
+ downloaded_bytes=total_size,
258
+ completed=True,
259
+ )
260
+ )
261
+
262
+ async def download_for_ollama(
263
+ self,
264
+ model_id: str,
265
+ quantization: str = "Q4_K_M",
266
+ progress_callback: Optional[Callable[[DownloadProgress], None]] = None,
267
+ ) -> DownloadResult:
268
+ """Download GGUF and create Ollama Modelfile.
269
+
270
+ Downloads the GGUF file and creates a Modelfile that can be used
271
+ with `ollama create` to register the model.
272
+
273
+ Args:
274
+ model_id: HuggingFace model ID with GGUF files
275
+ quantization: Desired quantization
276
+ progress_callback: Progress callback
277
+
278
+ Returns:
279
+ DownloadResult with ollama_model_name set
280
+ """
281
+ # Download the GGUF
282
+ result = await self.download_gguf(
283
+ model_id, quantization, progress_callback=progress_callback
284
+ )
285
+
286
+ if not result.success:
287
+ return result
288
+
289
+ # Create Modelfile
290
+ modelfile_path = result.path.parent / f"{result.path.stem}.Modelfile"
291
+
292
+ # Generate Ollama model name
293
+ model_name = model_id.split("/")[-1].lower()
294
+ model_name = model_name.replace("-gguf", "").replace("_gguf", "")
295
+ model_name = f"hf-{model_name}-{quantization.lower()}"
296
+
297
+ # Create Modelfile content
298
+ modelfile_content = f"""# Modelfile for {model_id}
299
+ # Created by SuperQode HF Downloader
300
+
301
+ FROM {result.path}
302
+
303
+ # Default parameters - adjust as needed
304
+ PARAMETER temperature 0.7
305
+ PARAMETER top_p 0.9
306
+ PARAMETER num_ctx 4096
307
+ """
308
+
309
+ with open(modelfile_path, "w") as f:
310
+ f.write(modelfile_content)
311
+
312
+ result.ollama_model_name = model_name
313
+
314
+ return result
315
+
316
+ async def register_with_ollama(
317
+ self, gguf_path: Path, model_name: str, parameters: Optional[Dict[str, Any]] = None
318
+ ) -> bool:
319
+ """Register a downloaded GGUF with Ollama.
320
+
321
+ Creates a Modelfile and runs `ollama create` to register the model.
322
+
323
+ Args:
324
+ gguf_path: Path to GGUF file
325
+ model_name: Name to register in Ollama
326
+ parameters: Optional parameters for Modelfile
327
+
328
+ Returns:
329
+ True if registration succeeded
330
+ """
331
+ # Create Modelfile
332
+ modelfile_content = f"FROM {gguf_path}\n"
333
+
334
+ if parameters:
335
+ for key, value in parameters.items():
336
+ modelfile_content += f"PARAMETER {key} {value}\n"
337
+ else:
338
+ modelfile_content += "PARAMETER temperature 0.7\n"
339
+ modelfile_content += "PARAMETER num_ctx 4096\n"
340
+
341
+ modelfile_path = gguf_path.parent / f"{model_name}.Modelfile"
342
+
343
+ with open(modelfile_path, "w") as f:
344
+ f.write(modelfile_content)
345
+
346
+ # Run ollama create
347
+ try:
348
+ result = subprocess.run(
349
+ ["ollama", "create", model_name, "-f", str(modelfile_path)],
350
+ capture_output=True,
351
+ text=True,
352
+ timeout=300,
353
+ )
354
+
355
+ return result.returncode == 0
356
+
357
+ except FileNotFoundError:
358
+ # Ollama not installed
359
+ return False
360
+ except subprocess.TimeoutExpired:
361
+ return False
362
+ except Exception:
363
+ return False
364
+
365
+ async def download_for_transformers(
366
+ self, model_id: str, progress_callback: Optional[Callable[[DownloadProgress], None]] = None
367
+ ) -> DownloadResult:
368
+ """Download a model for use with transformers.
369
+
370
+ This uses huggingface_hub's snapshot_download if available,
371
+ otherwise falls back to manual download.
372
+
373
+ Args:
374
+ model_id: HuggingFace model ID
375
+ progress_callback: Progress callback
376
+
377
+ Returns:
378
+ DownloadResult with path to model directory
379
+ """
380
+ try:
381
+ from huggingface_hub import snapshot_download
382
+
383
+ loop = asyncio.get_event_loop()
384
+
385
+ def do_download():
386
+ return snapshot_download(
387
+ model_id,
388
+ token=self._token,
389
+ cache_dir=str(self._cache_dir / "transformers"),
390
+ )
391
+
392
+ path = await loop.run_in_executor(None, do_download)
393
+
394
+ return DownloadResult(
395
+ success=True,
396
+ path=Path(path),
397
+ model_id=model_id,
398
+ )
399
+
400
+ except ImportError:
401
+ return DownloadResult(
402
+ success=False,
403
+ model_id=model_id,
404
+ error="huggingface_hub not installed. Run: pip install huggingface-hub",
405
+ )
406
+ except Exception as e:
407
+ return DownloadResult(success=False, model_id=model_id, error=str(e))
408
+
409
+ def list_cached_models(self) -> List[Dict[str, Any]]:
410
+ """List all cached downloaded models.
411
+
412
+ Returns:
413
+ List of dicts with cached model info.
414
+ """
415
+ cached = []
416
+
417
+ # Check GGUF cache
418
+ gguf_dir = self._cache_dir / "gguf"
419
+ if gguf_dir.exists():
420
+ for f in gguf_dir.glob("*.gguf"):
421
+ cached.append(
422
+ {
423
+ "type": "gguf",
424
+ "filename": f.name,
425
+ "path": str(f),
426
+ "size_bytes": f.stat().st_size,
427
+ "size_display": self._format_size(f.stat().st_size),
428
+ }
429
+ )
430
+
431
+ # Check transformers cache
432
+ tf_dir = self._cache_dir / "transformers"
433
+ if tf_dir.exists():
434
+ for d in tf_dir.iterdir():
435
+ if d.is_dir():
436
+ # Calculate total size
437
+ total_size = sum(f.stat().st_size for f in d.rglob("*") if f.is_file())
438
+ cached.append(
439
+ {
440
+ "type": "transformers",
441
+ "model_id": d.name,
442
+ "path": str(d),
443
+ "size_bytes": total_size,
444
+ "size_display": self._format_size(total_size),
445
+ }
446
+ )
447
+
448
+ return cached
449
+
450
+ def _format_size(self, size_bytes: int) -> str:
451
+ """Format size in human-readable format."""
452
+ gb = size_bytes / (1024**3)
453
+ if gb >= 1:
454
+ return f"{gb:.1f}GB"
455
+ mb = size_bytes / (1024**2)
456
+ return f"{mb:.0f}MB"
457
+
458
+
459
+ # Singleton instance
460
+ _downloader_instance: Optional[HFDownloader] = None
461
+
462
+
463
+ def get_hf_downloader() -> HFDownloader:
464
+ """Get the global HF downloader instance.
465
+
466
+ Returns:
467
+ HFDownloader instance.
468
+ """
469
+ global _downloader_instance
470
+ if _downloader_instance is None:
471
+ _downloader_instance = HFDownloader()
472
+ return _downloader_instance