k-cli-for-devs 1.0.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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1109 @@
1
+ """
2
+ model_manager.py - Model Bootstrapper & Auto-Sync Engine for K-CLI (Project Bankai)
3
+
4
+ Manages automated model lifecycle:
5
+ 1. Pulls quantized Bankai GGUF models (e.g. krishivjoshi/bankai-7b, krishivjoshi/bankai-10b)
6
+ from Hugging Face Hub directly into Ollama or local GGUF cache (~/.kcli/models, ~/models).
7
+ 2. Verifies SHA256 cryptographic integrity of downloaded GGUF binaries.
8
+ 3. Checks local Ollama instance health and auto-creates Ollama models via Modelfiles.
9
+ 4. Seamlessly integrates with K-CLI LLM Driver and Typer CLI interface.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ import logging
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import urllib.error
22
+ import urllib.parse
23
+ import urllib.request
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
27
+
28
+ logger = logging.getLogger("k_cli.model_manager")
29
+
30
+ # ------------------------------------------------------------------------------
31
+ # Constants & Defaults
32
+ # ------------------------------------------------------------------------------
33
+ DEFAULT_OLLAMA_URL = "http://localhost:11434"
34
+ DEFAULT_KCLI_DIR = Path.home() / ".kcli"
35
+ DEFAULT_MODELS_DIR = DEFAULT_KCLI_DIR / "models"
36
+ FALLBACK_MODELS_DIR = Path.home() / "models"
37
+
38
+ # Standard ChatML Prompt Template for Bankai Qwen-based models
39
+ DEFAULT_CHATML_TEMPLATE = """<|im_start|>system
40
+ {{ .System }}<|im_end|>
41
+ <|im_start|>user
42
+ {{ .Prompt }}<|im_end|>
43
+ <|im_start|>assistant
44
+ {{ .Response }}<|im_end|>
45
+ """
46
+
47
+ # Preset Model Catalog
48
+ MODEL_CATALOG: Dict[str, Dict[str, Any]] = {
49
+ "bankai-7b": {
50
+ "repo_id": "krishivjoshi/bankai-7b",
51
+ "default_filename": "bankai-7b.gguf",
52
+ "candidate_filenames": [
53
+ "bankai-7b.gguf",
54
+ "bankai-7b-q4_k_m.gguf",
55
+ "bankai-7b.Q4_K_M.gguf",
56
+ "qwen2.5-coder-7b-instruct.Q4_K_M.gguf",
57
+ ],
58
+ "ollama_tag": "bankai:7b",
59
+ "aliases": ["bankai-7b", "bankai:7b", "7b", "krishivjoshi/bankai-7b"],
60
+ "system_prompt": (
61
+ "You are Bankai-7B, an elite compiler-grounded AI coding model operating under strict "
62
+ "1.0 GB RAM constraints. You specialize in complex AST code generation, surgical "
63
+ "SEARCH/REPLACE patches, zero-fluff critique, and step-by-step technical reasoning inside <think>...</think> tags."
64
+ ),
65
+ "temperature": 0.2,
66
+ "top_p": 0.95,
67
+ "repeat_penalty": 1.1,
68
+ "stop_tokens": ["<|im_start|>", "<|im_end|>"],
69
+ },
70
+ "bankai-10b": {
71
+ "repo_id": "krishivjoshi/bankai-10b",
72
+ "default_filename": "bankai-10b.gguf",
73
+ "candidate_filenames": [
74
+ "bankai-10b.gguf",
75
+ "bankai-10b-q4_k_m.gguf",
76
+ "bankai-10b.Q4_K_M.gguf",
77
+ "qwen2.5-coder-10b-instruct.Q4_K_M.gguf",
78
+ ],
79
+ "ollama_tag": "bankai:10b",
80
+ "aliases": ["bankai-10b", "bankai:10b", "10b", "krishivjoshi/bankai-10b"],
81
+ "system_prompt": (
82
+ "You are Bankai-10B, a high-capacity compiler-grounded AI coding model operating under strict "
83
+ "RAM constraints. You specialize in multi-file architecture reasoning, surgical AST patches, "
84
+ "and formal verification inside <think>...</think> tags."
85
+ ),
86
+ "temperature": 0.2,
87
+ "top_p": 0.95,
88
+ "repeat_penalty": 1.1,
89
+ "stop_tokens": ["<|im_start|>", "<|im_end|>"],
90
+ },
91
+ "bankai-1.5b": {
92
+ "repo_id": "krishivjoshi/bankai-1.5b",
93
+ "default_filename": "bankai-1.5b.gguf",
94
+ "candidate_filenames": [
95
+ "bankai-1.5b.gguf",
96
+ "bankai-1.5b-q4_k_m.gguf",
97
+ "qwen2.5-coder-1.5b-instruct.Q4_K_M.gguf",
98
+ ],
99
+ "ollama_tag": "bankai:1.5b",
100
+ "aliases": ["bankai-1.5b", "bankai:1.5b", "1.5b", "krishivjoshi/bankai-1.5b"],
101
+ "system_prompt": (
102
+ "You are Bankai-1.5B, a fast lightweight compiler-grounded AI coding model operating under strict "
103
+ "1.0 GB RAM constraints. You specialize in AST code generation and surgical patches inside <think>...</think> tags."
104
+ ),
105
+ "temperature": 0.2,
106
+ "top_p": 0.95,
107
+ "repeat_penalty": 1.1,
108
+ "stop_tokens": ["<|im_start|>", "<|im_end|>"],
109
+ },
110
+ "bankai-3b": {
111
+ "repo_id": "krishivjoshi/bankai-3b",
112
+ "default_filename": "bankai-3b.gguf",
113
+ "candidate_filenames": [
114
+ "bankai-3b.gguf",
115
+ "bankai-3b-q4_k_m.gguf",
116
+ "qwen2.5-coder-3b-instruct.Q4_K_M.gguf",
117
+ ],
118
+ "ollama_tag": "bankai:3b",
119
+ "aliases": ["bankai-3b", "bankai:3b", "3b", "krishivjoshi/bankai-3b"],
120
+ "system_prompt": (
121
+ "You are Bankai-3B, an elite compiler-grounded AI coding model operating under strict "
122
+ "1.0 GB RAM constraints. You specialize in unpadded code generation, surgical SEARCH/REPLACE patches, "
123
+ "AST syntax validation, and step-by-step technical reasoning inside <think>...</think> tags."
124
+ ),
125
+ "temperature": 0.2,
126
+ "top_p": 0.95,
127
+ "repeat_penalty": 1.1,
128
+ "stop_tokens": ["<|im_start|>", "<|im_end|>"],
129
+ },
130
+ }
131
+
132
+
133
+ @dataclass
134
+ class ModelPullResult:
135
+ """Structured result from a model pull operation."""
136
+ success: bool
137
+ model_name: str
138
+ ollama_tag: str
139
+ gguf_path: Optional[Path] = None
140
+ modelfile_path: Optional[Path] = None
141
+ sha256: Optional[str] = None
142
+ sha256_verified: bool = False
143
+ ollama_created: bool = False
144
+ ollama_healthy: bool = False
145
+ message: str = ""
146
+ details: Dict[str, Any] = field(default_factory=dict)
147
+
148
+ def to_dict(self) -> Dict[str, Any]:
149
+ return {
150
+ "success": self.success,
151
+ "model_name": self.model_name,
152
+ "ollama_tag": self.ollama_tag,
153
+ "gguf_path": str(self.gguf_path) if self.gguf_path else None,
154
+ "modelfile_path": str(self.modelfile_path) if self.modelfile_path else None,
155
+ "sha256": self.sha256,
156
+ "sha256_verified": self.sha256_verified,
157
+ "ollama_created": self.ollama_created,
158
+ "ollama_healthy": self.ollama_healthy,
159
+ "message": self.message,
160
+ "details": self.details,
161
+ }
162
+
163
+
164
+ class ModelManager:
165
+ """
166
+ Automated Model Lifecycle, GGUF Cache, Integrity Verification, and Ollama Registration Engine.
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ models_dir: Optional[Union[str, Path]] = None,
172
+ ollama_url: Optional[str] = None,
173
+ mock_mode: Optional[bool] = None,
174
+ ):
175
+ if models_dir is not None:
176
+ self.models_dir = Path(models_dir).resolve()
177
+ else:
178
+ self.models_dir = DEFAULT_MODELS_DIR
179
+
180
+ # Ensure model cache directory exists
181
+ self.models_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ env_ollama = os.getenv("OLLAMA_HOST")
184
+ self.ollama_url = (ollama_url or env_ollama or DEFAULT_OLLAMA_URL).rstrip("/")
185
+ if not self.ollama_url.startswith("http"):
186
+ self.ollama_url = f"http://{self.ollama_url}"
187
+
188
+ if mock_mode is not None:
189
+ self.mock_mode = bool(mock_mode)
190
+ else:
191
+ self.mock_mode = os.getenv("KCLI_MOCK_MODE", "").lower() in ("true", "1") or (
192
+ "PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")
193
+ )
194
+
195
+ # Internal mock state for testing
196
+ self._mock_ollama_models: List[str] = ["qwen2.5-coder:1.5b", "bankai:1.5b"]
197
+
198
+ # --------------------------------------------------------------------------
199
+ # 1. Model Resolution & Spec Lookup
200
+ # --------------------------------------------------------------------------
201
+
202
+ @staticmethod
203
+ def resolve_model_spec(model_identifier: str) -> Dict[str, Any]:
204
+ """
205
+ Resolves model metadata and parameters for a given name, alias, or HF repo.
206
+ """
207
+ clean_id = model_identifier.strip().lower()
208
+
209
+ # Exact key or alias match
210
+ for key, spec in MODEL_CATALOG.items():
211
+ if clean_id == key or clean_id in spec.get("aliases", []):
212
+ return dict(spec)
213
+
214
+ # Partial matching (e.g. '7b' -> 'bankai-7b')
215
+ for key, spec in MODEL_CATALOG.items():
216
+ if clean_id in key or any(clean_id == a.lower() for a in spec.get("aliases", [])):
217
+ return dict(spec)
218
+
219
+ # Fallback for custom Hugging Face repository or custom model tag
220
+ repo_id = model_identifier if "/" in model_identifier else f"krishivjoshi/{model_identifier}"
221
+ clean_name = model_identifier.split("/")[-1].replace(":", "-")
222
+ tag_name = model_identifier.split("/")[-1].replace("-", ":")
223
+ return {
224
+ "repo_id": repo_id,
225
+ "default_filename": f"{clean_name}.gguf",
226
+ "candidate_filenames": [f"{clean_name}.gguf", f"{clean_name}-q4_k_m.gguf", f"{clean_name}.Q4_K_M.gguf"],
227
+ "ollama_tag": tag_name,
228
+ "aliases": [model_identifier, clean_name, tag_name],
229
+ "system_prompt": (
230
+ f"You are {clean_name.upper()}, an elite compiler-grounded AI coding model operating under "
231
+ "strict RAM constraints. You specialize in AST code generation and surgical patches inside <think>...</think> tags."
232
+ ),
233
+ "temperature": 0.2,
234
+ "top_p": 0.95,
235
+ "repeat_penalty": 1.1,
236
+ "stop_tokens": ["<|im_start|>", "<|im_end|>"],
237
+ }
238
+
239
+ # --------------------------------------------------------------------------
240
+ # 2. Ollama Health & Model Registration
241
+ # --------------------------------------------------------------------------
242
+
243
+ def check_ollama_health(self) -> Dict[str, Any]:
244
+ """
245
+ Checks local Ollama service availability, version, and loaded models.
246
+ """
247
+ if self.mock_mode:
248
+ return {
249
+ "healthy": True,
250
+ "version": "0.1.0-mock",
251
+ "models": list(self._mock_ollama_models),
252
+ "url": self.ollama_url,
253
+ "error": None,
254
+ }
255
+
256
+ health_data: Dict[str, Any] = {
257
+ "healthy": False,
258
+ "version": "unknown",
259
+ "models": [],
260
+ "url": self.ollama_url,
261
+ "error": None,
262
+ }
263
+
264
+ # 1. Check /api/tags
265
+ try:
266
+ req = urllib.request.Request(f"{self.ollama_url}/api/tags", method="GET")
267
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
268
+ if resp.status == 200:
269
+ data = json.loads(resp.read().decode("utf-8"))
270
+ models = [m.get("name", "") for m in data.get("models", []) if isinstance(m, dict)]
271
+ health_data["healthy"] = True
272
+ health_data["models"] = [m for m in models if m]
273
+ except Exception as e:
274
+ health_data["error"] = str(e)
275
+ return health_data
276
+
277
+ # 2. Check /api/version if available
278
+ try:
279
+ req_ver = urllib.request.Request(f"{self.ollama_url}/api/version", method="GET")
280
+ with urllib.request.urlopen(req_ver, timeout=2.0) as resp:
281
+ if resp.status == 200:
282
+ v_data = json.loads(resp.read().decode("utf-8"))
283
+ health_data["version"] = v_data.get("version", "unknown")
284
+ except Exception:
285
+ pass
286
+
287
+ return health_data
288
+
289
+ def is_ollama_available(self) -> bool:
290
+ """Returns True if local Ollama daemon is active and responsive."""
291
+ return self.check_ollama_health().get("healthy", False)
292
+
293
+ def list_ollama_models(self) -> List[str]:
294
+ """Returns list of all model tags available in Ollama."""
295
+ return self.check_ollama_health().get("models", [])
296
+
297
+ def has_ollama_model(self, model_tag: str) -> bool:
298
+ """Checks whether a specific model tag exists in Ollama."""
299
+ models = self.list_ollama_models()
300
+ target = model_tag.strip().lower()
301
+ for m in models:
302
+ m_low = m.lower()
303
+ if target == m_low or m_low.startswith(target) or target.startswith(m_low.split(":")[0]):
304
+ return True
305
+ # Also check without tag suffix (e.g. bankai-7b vs bankai:7b)
306
+ norm_target = target.replace("-", ":")
307
+ norm_m = m_low.replace("-", ":")
308
+ if norm_target == norm_m or norm_target.split(":")[0] == norm_m.split(":")[0]:
309
+ if ":" in target and ":" in m_low and target.split(":")[-1] == m_low.split(":")[-1]:
310
+ return True
311
+ return False
312
+
313
+ # --------------------------------------------------------------------------
314
+ # 3. Cryptographic SHA256 Integrity Verification
315
+ # --------------------------------------------------------------------------
316
+
317
+ @staticmethod
318
+ def compute_sha256(
319
+ file_path: Union[str, Path],
320
+ chunk_size: int = 1024 * 1024 * 4, # 4MB chunks
321
+ progress_callback: Optional[Callable[[int, int], None]] = None,
322
+ ) -> str:
323
+ """
324
+ Computes SHA256 hash of a local binary file.
325
+ """
326
+ fp = Path(file_path)
327
+ if not fp.exists() or not fp.is_file():
328
+ raise FileNotFoundError(f"Cannot compute SHA256: file '{fp}' not found.")
329
+
330
+ total_size = fp.stat().st_size
331
+ bytes_read = 0
332
+ sha256_hasher = hashlib.sha256()
333
+
334
+ with open(fp, "rb") as f:
335
+ while True:
336
+ chunk = f.read(chunk_size)
337
+ if not chunk:
338
+ break
339
+ sha256_hasher.update(chunk)
340
+ bytes_read += len(chunk)
341
+ if progress_callback:
342
+ progress_callback(bytes_read, total_size)
343
+
344
+ return sha256_hasher.hexdigest().lower()
345
+
346
+ @classmethod
347
+ def verify_sha256(
348
+ cls,
349
+ file_path: Union[str, Path],
350
+ expected_sha256: str,
351
+ chunk_size: int = 1024 * 1024 * 4,
352
+ ) -> bool:
353
+ """
354
+ Validates whether file matches expected SHA256 digest.
355
+ """
356
+ if not expected_sha256 or not expected_sha256.strip():
357
+ return False
358
+ calculated = cls.compute_sha256(file_path, chunk_size=chunk_size)
359
+ return calculated.lower() == expected_sha256.strip().lower()
360
+
361
+ def fetch_hf_metadata(
362
+ self,
363
+ repo_id: str,
364
+ filename: Optional[str] = None,
365
+ hf_token: Optional[str] = None,
366
+ ) -> Dict[str, Any]:
367
+ """
368
+ Queries Hugging Face API to extract remote file list and LFS SHA256 metadata.
369
+ """
370
+ if self.mock_mode:
371
+ return {
372
+ "repo_id": repo_id,
373
+ "files": [filename or "model.gguf"],
374
+ "lfs_sha256": {"bankai-7b.gguf": "mock_sha256_bankai_7b", "bankai-10b.gguf": "mock_sha256_bankai_10b"},
375
+ "size_map": {filename or "model.gguf": 4000000000},
376
+ }
377
+
378
+ token = hf_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
379
+ meta_url = f"https://huggingface.co/api/models/{repo_id}"
380
+ headers = {"User-Agent": "K-CLI/0.2.0 ModelManager"}
381
+ if token:
382
+ headers["Authorization"] = f"Bearer {token}"
383
+
384
+ try:
385
+ req = urllib.request.Request(meta_url, headers=headers, method="GET")
386
+ with urllib.request.urlopen(req, timeout=10.0) as resp:
387
+ if resp.status == 200:
388
+ data = json.loads(resp.read().decode("utf-8"))
389
+ siblings = data.get("siblings", [])
390
+ file_names = [s.get("rfilename", "") for s in siblings if isinstance(s, dict)]
391
+ lfs_map: Dict[str, str] = {}
392
+ size_map: Dict[str, int] = {}
393
+
394
+ # Inspect siblings or tags for LFS hashes
395
+ for s in siblings:
396
+ fn = s.get("rfilename", "")
397
+ lfs = s.get("lfs", {})
398
+ if isinstance(lfs, dict) and "sha256" in lfs:
399
+ lfs_map[fn] = lfs["sha256"]
400
+ if isinstance(lfs, dict) and "size" in lfs:
401
+ size_map[fn] = lfs["size"]
402
+
403
+ return {
404
+ "repo_id": repo_id,
405
+ "files": file_names,
406
+ "lfs_sha256": lfs_map,
407
+ "size_map": size_map,
408
+ }
409
+ except Exception as e:
410
+ logger.debug(f"HF API metadata lookup notice for {repo_id}: {e}")
411
+
412
+ return {"repo_id": repo_id, "files": [], "lfs_sha256": {}, "size_map": {}}
413
+
414
+ # --------------------------------------------------------------------------
415
+ # 4. GGUF Location & Hugging Face Hub Pull
416
+ # --------------------------------------------------------------------------
417
+
418
+ def find_local_gguf(self, model_identifier: str) -> Optional[Path]:
419
+ """
420
+ Searches local storage directories for existing GGUF binaries.
421
+ """
422
+ spec = self.resolve_model_spec(model_identifier)
423
+ candidates = spec.get("candidate_filenames", [spec.get("default_filename", f"{model_identifier}.gguf")])
424
+
425
+ search_dirs = [
426
+ self.models_dir,
427
+ FALLBACK_MODELS_DIR,
428
+ DEFAULT_KCLI_DIR,
429
+ Path("/content"), # Colab runtime root
430
+ Path("/content/bankai_7b_model"),
431
+ Path("/content/bankai_10b_model"),
432
+ ]
433
+
434
+ for s_dir in search_dirs:
435
+ if not s_dir.exists():
436
+ continue
437
+ for cand in candidates:
438
+ p = s_dir / cand
439
+ if p.exists() and p.is_file() and p.stat().st_size > 1024:
440
+ return p
441
+
442
+ # Also check direct glob matches
443
+ clean_name = model_identifier.split("/")[-1].replace(":", "-").lower()
444
+ for f in s_dir.glob("*.gguf"):
445
+ if clean_name in f.name.lower():
446
+ return f
447
+
448
+ return None
449
+
450
+ def download_from_hf(
451
+ self,
452
+ repo_id: str,
453
+ filename: Optional[str] = None,
454
+ target_path: Optional[Path] = None,
455
+ hf_token: Optional[str] = None,
456
+ quant_preference: str = "q4_k_m",
457
+ progress_callback: Optional[Callable[[int, int], None]] = None,
458
+ ) -> Path:
459
+ """
460
+ Downloads GGUF weights directly from Hugging Face Hub.
461
+ """
462
+ token = hf_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
463
+
464
+ # Resolve candidate filename if not specified
465
+ resolved_filename = filename
466
+ if not resolved_filename:
467
+ # Check model metadata
468
+ meta = self.fetch_hf_metadata(repo_id, hf_token=token)
469
+ files = meta.get("files", [])
470
+ gguf_files = [f for f in files if f.endswith(".gguf")]
471
+ if gguf_files:
472
+ # Prefer requested quantization
473
+ pref = [f for f in gguf_files if quant_preference.lower() in f.lower()]
474
+ resolved_filename = pref[0] if pref else gguf_files[0]
475
+ else:
476
+ spec = self.resolve_model_spec(repo_id)
477
+ resolved_filename = spec.get("default_filename", "model.gguf")
478
+
479
+ # Target destination
480
+ if target_path is not None:
481
+ dest = Path(target_path).resolve()
482
+ else:
483
+ dest = self.models_dir / resolved_filename
484
+
485
+ dest.parent.mkdir(parents=True, exist_ok=True)
486
+
487
+ if self.mock_mode:
488
+ # In mock mode, write a lightweight mock binary file
489
+ if not dest.exists():
490
+ mock_content = (
491
+ b"GGUF\x03\x00\x00\x00" # Valid GGUF magic header
492
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
493
+ + f"Mock weights for {repo_id}/{resolved_filename}".encode("utf-8")
494
+ )
495
+ dest.write_bytes(mock_content)
496
+ if progress_callback:
497
+ progress_callback(len(dest.read_bytes()), len(dest.read_bytes()))
498
+ return dest
499
+
500
+ # Strategy 1: huggingface_hub library
501
+ try:
502
+ from huggingface_hub import hf_hub_download
503
+ downloaded = hf_hub_download(
504
+ repo_id=repo_id,
505
+ filename=resolved_filename,
506
+ token=token,
507
+ local_dir=str(self.models_dir),
508
+ local_dir_use_symlinks=False,
509
+ )
510
+ downloaded_path = Path(downloaded)
511
+ if downloaded_path.resolve() != dest.resolve():
512
+ shutil.copy2(downloaded_path, dest)
513
+ return dest
514
+ except Exception as e_hf:
515
+ logger.debug(f"huggingface_hub direct download fallback: {e_hf}")
516
+
517
+ # Strategy 2: Direct HTTP Streaming Download
518
+ download_url = f"https://huggingface.co/{repo_id}/resolve/main/{resolved_filename}"
519
+ headers = {"User-Agent": "K-CLI/0.2.0 ModelManager"}
520
+ if token:
521
+ headers["Authorization"] = f"Bearer {token}"
522
+
523
+ req = urllib.request.Request(download_url, headers=headers, method="GET")
524
+ with urllib.request.urlopen(req, timeout=120.0) as resp:
525
+ if resp.status != 200:
526
+ raise RuntimeError(f"Failed to download from HF ({download_url}): HTTP {resp.status}")
527
+
528
+ total_bytes = int(resp.headers.get("content-length", 0))
529
+ downloaded_bytes = 0
530
+
531
+ temp_dest = dest.with_suffix(".tmp")
532
+ with open(temp_dest, "wb") as f_out:
533
+ while True:
534
+ chunk = resp.read(1024 * 1024 * 4) # 4MB chunks
535
+ if not chunk:
536
+ break
537
+ f_out.write(chunk)
538
+ downloaded_bytes += len(chunk)
539
+ if progress_callback:
540
+ progress_callback(downloaded_bytes, total_bytes)
541
+
542
+ temp_dest.replace(dest)
543
+
544
+ return dest
545
+
546
+ # --------------------------------------------------------------------------
547
+ # 5. Modelfile Generation & Ollama Deployment
548
+ # --------------------------------------------------------------------------
549
+
550
+ def generate_modelfile(
551
+ self,
552
+ gguf_path: Union[str, Path],
553
+ model_name: str = "bankai-7b",
554
+ system_prompt: Optional[str] = None,
555
+ template: Optional[str] = None,
556
+ temperature: Optional[float] = None,
557
+ top_p: Optional[float] = None,
558
+ repeat_penalty: Optional[float] = None,
559
+ ) -> str:
560
+ """
561
+ Generates standard Modelfile content for Ollama model creation.
562
+ """
563
+ spec = self.resolve_model_spec(model_name)
564
+ gguf_abs = Path(gguf_path).resolve()
565
+
566
+ sys_prompt = system_prompt or spec.get("system_prompt", "")
567
+ tmpl = template or DEFAULT_CHATML_TEMPLATE
568
+ temp = temperature if temperature is not None else spec.get("temperature", 0.2)
569
+ tp = top_p if top_p is not None else spec.get("top_p", 0.95)
570
+ rp = repeat_penalty if repeat_penalty is not None else spec.get("repeat_penalty", 1.1)
571
+ stop_tokens = spec.get("stop_tokens", ["<|im_start|>", "<|im_end|>"])
572
+
573
+ lines = [
574
+ f"FROM {gguf_abs}",
575
+ "",
576
+ 'TEMPLATE """' + tmpl.strip() + '"""',
577
+ "",
578
+ ]
579
+
580
+ for st in stop_tokens:
581
+ lines.append(f'PARAMETER stop "{st}"')
582
+
583
+ lines.append(f"PARAMETER temperature {temp}")
584
+ lines.append(f"PARAMETER top_p {tp}")
585
+ lines.append(f"PARAMETER repeat_penalty {rp}")
586
+ lines.append("")
587
+ lines.append(f'SYSTEM """{sys_prompt.strip()}"""')
588
+ lines.append("")
589
+
590
+ return "\n".join(lines)
591
+
592
+ def write_modelfile(
593
+ self,
594
+ gguf_path: Union[str, Path],
595
+ output_modelfile_path: Optional[Union[str, Path]] = None,
596
+ model_name: str = "bankai-7b",
597
+ system_prompt: Optional[str] = None,
598
+ ) -> Path:
599
+ """
600
+ Writes generated Modelfile to disk and returns path.
601
+ """
602
+ content = self.generate_modelfile(
603
+ gguf_path=gguf_path,
604
+ model_name=model_name,
605
+ system_prompt=system_prompt,
606
+ )
607
+
608
+ if output_modelfile_path is not None:
609
+ out_p = Path(output_modelfile_path).resolve()
610
+ else:
611
+ clean_name = model_name.replace(":", "-").replace("/", "-")
612
+ out_p = self.models_dir / f"Modelfile.{clean_name}"
613
+
614
+ out_p.parent.mkdir(parents=True, exist_ok=True)
615
+ out_p.write_text(content, encoding="utf-8")
616
+ return out_p
617
+
618
+ def create_ollama_model(
619
+ self,
620
+ model_tag: str,
621
+ modelfile_path: Union[str, Path],
622
+ ) -> Tuple[bool, str]:
623
+ """
624
+ Registers model with local Ollama service using 'ollama create' CLI or API.
625
+ """
626
+ mf_path = Path(modelfile_path).resolve()
627
+ if not mf_path.exists():
628
+ return False, f"Modelfile not found at '{mf_path}'."
629
+
630
+ if self.mock_mode:
631
+ if model_tag not in self._mock_ollama_models:
632
+ self._mock_ollama_models.append(model_tag)
633
+ return True, f"Mock: Successfully created Ollama model '{model_tag}' from {mf_path.name}"
634
+
635
+ # Strategy 1: Ollama CLI (`ollama create <tag> -f <modelfile>`)
636
+ ollama_bin = shutil.which("ollama") or "/home/k/bin/ollama"
637
+ if os.path.exists(ollama_bin) and os.access(ollama_bin, os.X_OK):
638
+ try:
639
+ cmd = [ollama_bin, "create", model_tag, "-f", str(mf_path)]
640
+ res = subprocess.run(
641
+ cmd,
642
+ capture_output=True,
643
+ text=True,
644
+ timeout=300,
645
+ )
646
+ if res.returncode == 0:
647
+ return True, f"Successfully created Ollama model '{model_tag}' via CLI."
648
+ else:
649
+ logger.debug(f"Ollama CLI create failed (code {res.returncode}): {res.stderr}")
650
+ except Exception as e_cli:
651
+ logger.debug(f"Ollama CLI invocation error: {e_cli}")
652
+
653
+ # Strategy 2: Ollama HTTP REST API (`/api/create`)
654
+ try:
655
+ create_url = f"{self.ollama_url}/api/create"
656
+ modelfile_content = mf_path.read_text(encoding="utf-8")
657
+ payload = {
658
+ "name": model_tag,
659
+ "modelfile": modelfile_content,
660
+ "stream": False,
661
+ }
662
+ data = json.dumps(payload).encode("utf-8")
663
+ req = urllib.request.Request(
664
+ create_url,
665
+ data=data,
666
+ headers={"Content-Type": "application/json"},
667
+ method="POST",
668
+ )
669
+ with urllib.request.urlopen(req, timeout=300) as resp:
670
+ if resp.status == 200:
671
+ return True, f"Successfully created Ollama model '{model_tag}' via API."
672
+ return False, f"Ollama API returned HTTP {resp.status} during model creation."
673
+ except Exception as e_api:
674
+ return False, f"Failed to create Ollama model '{model_tag}': {e_api}"
675
+
676
+ # --------------------------------------------------------------------------
677
+ # 6. High-Level Automated Model Pull & Bootstrapping Pipeline
678
+ # --------------------------------------------------------------------------
679
+
680
+ def pull_model(
681
+ self,
682
+ model_identifier: str = "bankai-7b",
683
+ ollama_tag: Optional[str] = None,
684
+ hf_repo: Optional[str] = None,
685
+ force: bool = False,
686
+ verify_sha: bool = True,
687
+ create_in_ollama: bool = True,
688
+ expected_sha256: Optional[str] = None,
689
+ quant: str = "q4_k_m",
690
+ progress_callback: Optional[Callable[[int, int], None]] = None,
691
+ ) -> ModelPullResult:
692
+ """
693
+ Executes end-to-end model synchronization:
694
+ 1. Resolves Hugging Face repo & target model spec.
695
+ 2. Checks local cache or downloads GGUF weights directly from Hugging Face Hub.
696
+ 3. Computes and verifies SHA256 integrity against expected or remote LFS hash.
697
+ 4. Generates Modelfile and registers the model in Ollama.
698
+ """
699
+ spec = self.resolve_model_spec(model_identifier)
700
+ repo_id = hf_repo or spec.get("repo_id", f"krishivjoshi/{model_identifier}")
701
+ tag = ollama_tag or spec.get("ollama_tag", model_identifier)
702
+
703
+ # Check local cache first
704
+ local_gguf = self.find_local_gguf(model_identifier) if not force else None
705
+ downloaded = False
706
+
707
+ if local_gguf and local_gguf.exists() and not force:
708
+ gguf_path = local_gguf
709
+ logger.info(f"Using cached GGUF artifact: {gguf_path}")
710
+ else:
711
+ try:
712
+ gguf_path = self.download_from_hf(
713
+ repo_id=repo_id,
714
+ quant_preference=quant,
715
+ progress_callback=progress_callback,
716
+ )
717
+ downloaded = True
718
+ except Exception as dl_err:
719
+ return ModelPullResult(
720
+ success=False,
721
+ model_name=model_identifier,
722
+ ollama_tag=tag,
723
+ message=f"Failed to download '{repo_id}' from Hugging Face Hub: {dl_err}",
724
+ details={"error": str(dl_err)},
725
+ )
726
+
727
+ # Compute SHA256 hash
728
+ computed_sha = None
729
+ sha_verified = False
730
+ try:
731
+ computed_sha = self.compute_sha256(gguf_path)
732
+ target_sha = expected_sha256
733
+ if not target_sha:
734
+ # Attempt to query HF metadata for LFS hash
735
+ meta = self.fetch_hf_metadata(repo_id)
736
+ target_sha = meta.get("lfs_sha256", {}).get(gguf_path.name)
737
+
738
+ if target_sha:
739
+ sha_verified = (computed_sha.lower() == target_sha.strip().lower())
740
+ if verify_sha and not sha_verified:
741
+ return ModelPullResult(
742
+ success=False,
743
+ model_name=model_identifier,
744
+ ollama_tag=tag,
745
+ gguf_path=gguf_path,
746
+ sha256=computed_sha,
747
+ sha256_verified=False,
748
+ message=(
749
+ f"SHA256 integrity mismatch for '{gguf_path.name}'. "
750
+ f"Expected {target_sha}, got {computed_sha}"
751
+ ),
752
+ details={"expected_sha": target_sha, "computed_sha": computed_sha},
753
+ )
754
+ else:
755
+ if verify_sha and not self.mock_mode:
756
+ return ModelPullResult(
757
+ success=False,
758
+ model_name=model_identifier,
759
+ ollama_tag=tag,
760
+ gguf_path=gguf_path,
761
+ sha256=computed_sha,
762
+ sha256_verified=False,
763
+ message=(
764
+ f"No trusted SHA256 reference is available for '{gguf_path.name}'. "
765
+ "Refusing to install an unverified model."
766
+ ),
767
+ details={"computed_sha": computed_sha},
768
+ )
769
+ sha_verified = self.mock_mode
770
+ except Exception as sha_err:
771
+ logger.warning(f"SHA256 verification warning: {sha_err}")
772
+
773
+ # Generate Modelfile
774
+ modelfile_path = self.write_modelfile(
775
+ gguf_path=gguf_path,
776
+ model_name=model_identifier,
777
+ )
778
+
779
+ # Ollama Health & Model Registration
780
+ ollama_healthy = self.is_ollama_available()
781
+ ollama_created = False
782
+ create_msg = ""
783
+
784
+ if create_in_ollama:
785
+ if ollama_healthy:
786
+ ollama_created, create_msg = self.create_ollama_model(
787
+ model_tag=tag,
788
+ modelfile_path=modelfile_path,
789
+ )
790
+ else:
791
+ create_msg = f"Ollama daemon not reachable at {self.ollama_url}. Modelfile ready at {modelfile_path}."
792
+
793
+ success = (gguf_path.exists() and (not create_in_ollama or ollama_created or not ollama_healthy))
794
+
795
+ return ModelPullResult(
796
+ success=success,
797
+ model_name=model_identifier,
798
+ ollama_tag=tag,
799
+ gguf_path=gguf_path,
800
+ modelfile_path=modelfile_path,
801
+ sha256=computed_sha,
802
+ sha256_verified=sha_verified,
803
+ ollama_created=ollama_created,
804
+ ollama_healthy=ollama_healthy,
805
+ message=create_msg or f"Model '{model_identifier}' successfully staged at {gguf_path}",
806
+ details={
807
+ "downloaded": downloaded,
808
+ "repo_id": repo_id,
809
+ "quant": quant,
810
+ },
811
+ )
812
+
813
+ def init_environment(
814
+ self,
815
+ default_model: str = "bankai-7b",
816
+ sync_model: bool = True,
817
+ force: bool = False,
818
+ ) -> Dict[str, Any]:
819
+ """
820
+ Scaffolds the full K-CLI local directory hierarchy and provisions the default Bankai model.
821
+ """
822
+ # 1. Scaffolding directory tree
823
+ subdirs = ["docs", "repos", "logs", "khoj_index", "parsers", "models"]
824
+ created_dirs = []
825
+ for sd in subdirs:
826
+ d = DEFAULT_KCLI_DIR / sd
827
+ d.mkdir(parents=True, exist_ok=True)
828
+ created_dirs.append(str(d))
829
+
830
+ # 2. Check Ollama Health
831
+ ollama_status = self.check_ollama_health()
832
+
833
+ # 3. Pull / Sync default Bankai model
834
+ pull_res: Optional[ModelPullResult] = None
835
+ if sync_model:
836
+ pull_res = self.pull_model(
837
+ model_identifier=default_model,
838
+ force=force,
839
+ verify_sha=True,
840
+ create_in_ollama=True,
841
+ )
842
+
843
+ return {
844
+ "kcli_home": str(DEFAULT_KCLI_DIR),
845
+ "directories": created_dirs,
846
+ "ollama": ollama_status,
847
+ "model_pull": pull_res.to_dict() if pull_res else None,
848
+ "ready": True if (not sync_model or (pull_res and pull_res.success)) else False,
849
+ }
850
+
851
+ def pull_ollama_tag(
852
+ self,
853
+ model_tag: str,
854
+ progress_callback: Optional[Callable[[str], None]] = None,
855
+ ) -> Tuple[bool, str]:
856
+ """Pulls a model tag directly from Ollama registry (e.g. qwen2.5-coder:7b)."""
857
+ if self.mock_mode:
858
+ if model_tag not in self._mock_ollama_models:
859
+ self._mock_ollama_models.append(model_tag)
860
+ if progress_callback:
861
+ progress_callback(f"Mock: Pulled {model_tag} 100%")
862
+ return True, f"Mock: Pulled {model_tag} successfully."
863
+
864
+ # Check Ollama CLI
865
+ ollama_bin = shutil.which("ollama") or "/home/k/bin/ollama"
866
+ if os.path.exists(ollama_bin) and os.access(ollama_bin, os.X_OK):
867
+ try:
868
+ proc = subprocess.Popen(
869
+ [ollama_bin, "pull", model_tag],
870
+ stdout=subprocess.PIPE,
871
+ stderr=subprocess.STDOUT,
872
+ text=True,
873
+ )
874
+ if proc.stdout:
875
+ for line in proc.stdout:
876
+ if progress_callback and line.strip():
877
+ progress_callback(line.strip())
878
+ proc.wait(timeout=600)
879
+ if proc.returncode == 0:
880
+ return True, f"Successfully pulled {model_tag} into Ollama."
881
+ except Exception as e:
882
+ logger.debug(f"CLI pull error: {e}")
883
+
884
+ # Fallback to Ollama HTTP API /api/pull
885
+ try:
886
+ req = urllib.request.Request(
887
+ f"{self.ollama_url}/api/pull",
888
+ data=json.dumps({"name": model_tag, "stream": False}).encode("utf-8"),
889
+ headers={"Content-Type": "application/json", "User-Agent": "K-CLI"},
890
+ method="POST",
891
+ )
892
+ with urllib.request.urlopen(req, timeout=600.0) as resp:
893
+ if resp.status == 200:
894
+ return True, f"Successfully pulled {model_tag} via Ollama API."
895
+ except Exception as e:
896
+ return False, f"Failed to pull {model_tag}: {e}"
897
+
898
+ return False, f"Could not pull {model_tag}"
899
+
900
+
901
+ # ------------------------------------------------------------------------------
902
+ # Curated Local Coding Models Catalog with In-Depth Pros & Cons
903
+ # ------------------------------------------------------------------------------
904
+ LOCAL_CODING_MODELS: List[Dict[str, Any]] = [
905
+ {
906
+ "id": "qwen2.5-coder:7b",
907
+ "name": "Qwen 2.5 Coder 7B",
908
+ "size": "4.7 GB",
909
+ "ram": "8 GB RAM / 6 GB VRAM",
910
+ "context": "32K / 128K",
911
+ "speed": "~65 tok/s (GPU)",
912
+ "pros": [
913
+ "🏆 #1 Open-Weight Coding Model in 7B class (HumanEval 88.4%)",
914
+ "✨ Flawless Multi-File AST & Surgical SEARCH/REPLACE reasoning",
915
+ "⚡ Fast inference speed with low memory footprint",
916
+ "🌐 Supports 92+ programming languages including Python, Rust, C++, Go",
917
+ ],
918
+ "cons": [
919
+ "⚠️ Needs 6GB+ VRAM for full GPU offload",
920
+ "⚠️ May struggle with ultra-large monolithic repos without AST slicing",
921
+ ],
922
+ "hf_repo": "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF",
923
+ "ollama_tag": "qwen2.5-coder:7b",
924
+ },
925
+ {
926
+ "id": "qwen2.5-coder:14b",
927
+ "name": "Qwen 2.5 Coder 14B",
928
+ "size": "9.0 GB",
929
+ "ram": "16 GB RAM / 10 GB VRAM",
930
+ "context": "32K / 128K",
931
+ "speed": "~45 tok/s (GPU)",
932
+ "pros": [
933
+ "🧠 Rivals proprietary GPT-4o-mini and Claude 3.5 Haiku on coding evals",
934
+ "🏗️ Outstanding architectural design and refactoring synthesis",
935
+ "🛡️ High precision on type safety, unit tests, and edge cases",
936
+ ],
937
+ "cons": [
938
+ "⚠️ Requires 10GB+ VRAM (or 16GB Mac Unified Memory)",
939
+ "⚠️ Slower execution on pure CPU without AVX-512",
940
+ ],
941
+ "hf_repo": "Qwen/Qwen2.5-Coder-14B-Instruct-GGUF",
942
+ "ollama_tag": "qwen2.5-coder:14b",
943
+ },
944
+ {
945
+ "id": "qwen2.5-coder:1.5b",
946
+ "name": "Qwen 2.5 Coder 1.5B (Ultra-Light)",
947
+ "size": "1.0 GB",
948
+ "ram": "2 GB RAM / 1 GB VRAM",
949
+ "context": "32K",
950
+ "speed": "~140 tok/s (GPU) / ~45 tok/s (CPU)",
951
+ "pros": [
952
+ "🚀 Ultra-fast generation (<10ms time-to-first-token)",
953
+ "💾 Runs on any laptop, Raspberry Pi, or low-spec VM",
954
+ "💰 100% Free, zero CPU throttle, ideal for autocomplete & inline diffs",
955
+ ],
956
+ "cons": [
957
+ "⚠️ Limited reasoning depth for complex concurrency & race conditions",
958
+ "⚠️ Lower context capacity for multi-file refactors",
959
+ ],
960
+ "hf_repo": "Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF",
961
+ "ollama_tag": "qwen2.5-coder:1.5b",
962
+ },
963
+ {
964
+ "id": "deepseek-r1:7b",
965
+ "name": "DeepSeek-R1 Distill Qwen 7B (Reasoning)",
966
+ "size": "4.7 GB",
967
+ "ram": "8 GB RAM / 6 GB VRAM",
968
+ "context": "32K / 64K",
969
+ "speed": "~50 tok/s (GPU)",
970
+ "pros": [
971
+ "🧠 Deep Chain-of-Thought reasoning inside visible <think> blocks",
972
+ "🔬 Excellent at finding subtle bugs, race conditions, and cryptographic flaws",
973
+ "⚖️ Self-corrects mistakes before emitting final verified code",
974
+ ],
975
+ "cons": [
976
+ "⚠️ Slower total task time due to generating extensive thinking tokens",
977
+ "⚠️ Can overthink trivial tasks like docstrings or variable renames",
978
+ ],
979
+ "hf_repo": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B-GGUF",
980
+ "ollama_tag": "deepseek-r1:7b",
981
+ },
982
+ {
983
+ "id": "deepseek-r1:14b",
984
+ "name": "DeepSeek-R1 Distill Qwen 14B (Advanced Reasoning)",
985
+ "size": "9.0 GB",
986
+ "ram": "16 GB RAM / 10 GB VRAM",
987
+ "context": "32K / 64K",
988
+ "speed": "~35 tok/s (GPU)",
989
+ "pros": [
990
+ "🎓 Frontier-grade mathematical and algorithmic problem solving",
991
+ "🛡️ Compiler-level verification and multi-step theorem proving",
992
+ "🏆 Top-performing 14B model for complex full-stack architectures",
993
+ ],
994
+ "cons": [
995
+ "⚠️ High VRAM requirement (10GB+)",
996
+ "⚠️ Extended latency per response",
997
+ ],
998
+ "hf_repo": "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B-GGUF",
999
+ "ollama_tag": "deepseek-r1:14b",
1000
+ },
1001
+ {
1002
+ "id": "llama3.3:70b",
1003
+ "name": "Meta Llama 3.3 70B (Flagship)",
1004
+ "size": "42 GB (Q4_K_M)",
1005
+ "ram": "48 GB RAM / 40 GB VRAM",
1006
+ "context": "128K",
1007
+ "speed": "~25 tok/s (Multi-GPU)",
1008
+ "pros": [
1009
+ "👑 World-class general knowledge and massive 128k context window",
1010
+ "📚 Flawless technical documentation, architectural synthesis, and RFC generation",
1011
+ "⚡ Matches or exceeds GPT-4-0613 across software engineering evals",
1012
+ ],
1013
+ "cons": [
1014
+ "⚠️ Massive hardware requirements (requires dual RTX 3090/4090 or Mac Studio)",
1015
+ "⚠️ Unusable on low-spec single-GPU machines",
1016
+ ],
1017
+ "hf_repo": "meta-llama/Llama-3.3-70B-Instruct-GGUF",
1018
+ "ollama_tag": "llama3.3:70b",
1019
+ },
1020
+ {
1021
+ "id": "phi4:14b",
1022
+ "name": "Microsoft Phi-4 14B",
1023
+ "size": "9.1 GB",
1024
+ "ram": "16 GB RAM / 10 GB VRAM",
1025
+ "context": "16K",
1026
+ "speed": "~40 tok/s (GPU)",
1027
+ "pros": [
1028
+ "🔬 State-of-the-art synthetic data training by Microsoft Research",
1029
+ "📐 Dense algorithmic reasoning per parameter count",
1030
+ ],
1031
+ "cons": [
1032
+ "⚠️ Smaller 16k context window compared to Qwen 2.5",
1033
+ "⚠️ Strict system prompt sensitivity",
1034
+ ],
1035
+ "hf_repo": "microsoft/phi-4-gguf",
1036
+ "ollama_tag": "phi4:14b",
1037
+ },
1038
+ {
1039
+ "id": "starcoder2:15b",
1040
+ "name": "BigCode StarCoder2 15B",
1041
+ "size": "9.5 GB",
1042
+ "ram": "16 GB RAM / 10 GB VRAM",
1043
+ "context": "16K",
1044
+ "speed": "~40 tok/s (GPU)",
1045
+ "pros": [
1046
+ "📜 Fully open, permissively licensed training data (The Stack v2)",
1047
+ "🌐 600+ programming language coverage",
1048
+ ],
1049
+ "cons": [
1050
+ "⚠️ Weaker natural language chat instruction following",
1051
+ ],
1052
+ "hf_repo": "bigcode/starcoder2-15b-gguf",
1053
+ "ollama_tag": "starcoder2:15b",
1054
+ },
1055
+ ]
1056
+
1057
+ BANKAI_CUSTOM_MODELS: List[Dict[str, Any]] = [
1058
+ {
1059
+ "id": "bankai-7b",
1060
+ "name": "Bankai-7B GGUF (Flagship Code & AST Healer)",
1061
+ "repo_id": "krishivjoshi/bankai-7b",
1062
+ "default_filename": "bankai-7b.gguf",
1063
+ "size": "4.7 GB",
1064
+ "ram": "1.0-4.0 GB RAM Budget",
1065
+ "ollama_tag": "bankai:7b",
1066
+ "description": "Custom fine-tuned compiler-grounded model with AST patch synthesis, test generation, and <think> chain-of-thought.",
1067
+ },
1068
+ {
1069
+ "id": "bankai-10b",
1070
+ "name": "Bankai-10B GGUF (Multi-File Architecture)",
1071
+ "repo_id": "krishivjoshi/bankai-10b",
1072
+ "default_filename": "bankai-10b.gguf",
1073
+ "size": "6.5 GB",
1074
+ "ram": "6.0 GB RAM Budget",
1075
+ "ollama_tag": "bankai:10b",
1076
+ "description": "High-capacity architecture model for 10+ file scaffolding, 3-way merge conflict resolution, and PR reviews.",
1077
+ },
1078
+ {
1079
+ "id": "bankai-3b",
1080
+ "name": "Bankai-3B GGUF (Sovereign 1.0GB RAM Edition)",
1081
+ "repo_id": "krishivjoshi/bankai-3b",
1082
+ "default_filename": "bankai-3b.gguf",
1083
+ "size": "2.2 GB",
1084
+ "ram": "1.0 GB RAM Budget",
1085
+ "ollama_tag": "bankai:3b",
1086
+ "description": "Engineered strictly for memory-constrained environments, edge devices, and 100% offline air-gapped development.",
1087
+ },
1088
+ {
1089
+ "id": "bankai-1.5b",
1090
+ "name": "Bankai-1.5B GGUF (Fast Background Daemon)",
1091
+ "repo_id": "krishivjoshi/bankai-1.5b",
1092
+ "default_filename": "bankai-1.5b.gguf",
1093
+ "size": "1.1 GB",
1094
+ "ram": "512 MB - 1.0 GB RAM",
1095
+ "ollama_tag": "bankai:1.5b",
1096
+ "description": "Ultra-lightweight background model for Ghost crash interception, conventional commit generation, and real-time linting.",
1097
+ },
1098
+ ]
1099
+
1100
+
1101
+ def list_local_coding_models() -> List[Dict[str, Any]]:
1102
+ """Returns the list of curated local models with pros/cons."""
1103
+ return list(LOCAL_CODING_MODELS)
1104
+
1105
+
1106
+ def list_bankai_models() -> List[Dict[str, Any]]:
1107
+ """Returns the list of custom Bankai models on Hugging Face."""
1108
+ return list(BANKAI_CUSTOM_MODELS)
1109
+