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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|