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,913 @@
1
+ """
2
+ models_hub.py - Universal AI Model Hub & Multi-Provider Model Registry for K-CLI
3
+ Project Bankai Engine v1.0.0
4
+
5
+ Supports dynamic discovery, configuration, pulling, benchmarking, and cascading for:
6
+ 1. Local Providers:
7
+ - Ollama (Qwen2.5-Coder, DeepSeek-R1, Llama-3.3, StarCoder2, CodeLlama, Phi-4, Mistral)
8
+ - llama.cpp HTTP server (GGUF weights)
9
+ - Native llama-cpp-python GGUF in-process
10
+ - vLLM / SGLang / LocalAI / LM Studio / Jan (OpenAI-compatible local endpoints)
11
+ 2. Cloud Model Providers:
12
+ - Google Gemini (Gemini 2.0 Flash, Gemini 2.0 Pro, Gemini 1.5 Pro, Thinking models)
13
+ - Anthropic (Claude 3.7 Sonnet, Claude 3.5 Sonnet, Claude 3.5 Haiku)
14
+ - OpenAI (GPT-4o, GPT-4o-mini, o1, o3-mini)
15
+ - DeepSeek (DeepSeek V3, DeepSeek R1)
16
+ - Groq (Ultra-fast Llama-3.3-70B, Qwen-2.5-Coder-32B @ 300+ tok/s)
17
+ - Mistral AI (Codestral, Mistral Large)
18
+ - OpenRouter (Unified access to 100+ AI models)
19
+ - Together AI, Cerebras, Fireworks, Cohere
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import os
27
+ import re
28
+ import shutil
29
+ import subprocess
30
+ import time
31
+ import urllib.error
32
+ import urllib.request
33
+ from dataclasses import dataclass, field
34
+ from enum import Enum
35
+ from pathlib import Path
36
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
37
+
38
+ logger = logging.getLogger("k_cli.models_hub")
39
+
40
+
41
+ class ModelProvider(str, Enum):
42
+ OLLAMA = "ollama"
43
+ LLAMACPP = "llamacpp"
44
+ NATIVE = "native"
45
+ VLLM = "vllm"
46
+ LMSTUDIO = "lmstudio"
47
+ LOCALAI = "localai"
48
+ OPENAI = "openai"
49
+ ANTHROPIC = "anthropic"
50
+ GEMINI = "gemini"
51
+ DEEPSEEK = "deepseek"
52
+ GROQ = "groq"
53
+ MISTRAL = "mistral"
54
+ OPENROUTER = "openrouter"
55
+ TOGETHER = "together"
56
+ MOCK = "mock"
57
+
58
+
59
+ @dataclass
60
+ class ModelSpec:
61
+ """Specification and metadata for an AI model."""
62
+ id: str
63
+ name: str
64
+ provider: ModelProvider
65
+ context_window: int = 32768
66
+ max_output_tokens: int = 4096
67
+ is_local: bool = False
68
+ description: str = ""
69
+ prompt_price_per_m: float = 0.0
70
+ completion_price_per_m: float = 0.0
71
+ supports_tools: bool = True
72
+ supports_vision: bool = False
73
+ supports_thinking: bool = False
74
+ is_installed: bool = False
75
+ base_url: Optional[str] = None
76
+ env_var_key: Optional[str] = None
77
+
78
+ def to_dict(self) -> Dict[str, Any]:
79
+ return {
80
+ "id": self.id,
81
+ "name": self.name,
82
+ "provider": self.provider.value,
83
+ "context_window": self.context_window,
84
+ "max_output_tokens": self.max_output_tokens,
85
+ "is_local": self.is_local,
86
+ "description": self.description,
87
+ "prompt_price_per_m": self.prompt_price_per_m,
88
+ "completion_price_per_m": self.completion_price_per_m,
89
+ "supports_tools": self.supports_tools,
90
+ "supports_vision": self.supports_vision,
91
+ "supports_thinking": self.supports_thinking,
92
+ "is_installed": self.is_installed,
93
+ "base_url": self.base_url,
94
+ "env_var_key": self.env_var_key,
95
+ }
96
+
97
+
98
+ @dataclass
99
+ class ModelBenchmarkResult:
100
+ """Telemetry results from model execution benchmark."""
101
+ model_id: str
102
+ provider: str
103
+ success: bool
104
+ tokens_generated: int = 0
105
+ duration_seconds: float = 0.0
106
+ tokens_per_second: float = 0.0
107
+ time_to_first_token: float = 0.0
108
+ ram_rss_mb: float = 0.0
109
+ error_message: Optional[str] = None
110
+ sample_output: str = ""
111
+
112
+ def to_dict(self) -> Dict[str, Any]:
113
+ return {
114
+ "model_id": self.model_id,
115
+ "provider": self.provider,
116
+ "success": self.success,
117
+ "tokens_generated": self.tokens_generated,
118
+ "duration_seconds": round(self.duration_seconds, 4),
119
+ "tokens_per_second": round(self.tokens_per_second, 2),
120
+ "time_to_first_token": round(self.time_to_first_token, 4),
121
+ "ram_rss_mb": round(self.ram_rss_mb, 2),
122
+ "error_message": self.error_message,
123
+ "sample_output": self.sample_output,
124
+ }
125
+
126
+
127
+ # Curated catalog of industry-leading local and cloud models
128
+ MODEL_CATALOG_REGISTRY: Dict[str, ModelSpec] = {
129
+ # --- Local Models (Ollama / GGUF) ---
130
+ "qwen2.5-coder:1.5b": ModelSpec(
131
+ id="qwen2.5-coder:1.5b",
132
+ name="Qwen 2.5 Coder 1.5B (Ultra-Light)",
133
+ provider=ModelProvider.OLLAMA,
134
+ context_window=32768,
135
+ is_local=True,
136
+ description="Fastest lightweight local coding model",
137
+ ),
138
+ "qwen2.5-coder:7b": ModelSpec(
139
+ id="qwen2.5-coder:7b",
140
+ name="Qwen 2.5 Coder 7B",
141
+ provider=ModelProvider.OLLAMA,
142
+ context_window=32768,
143
+ is_local=True,
144
+ description="Strong local coding and multi-file reasoning model",
145
+ ),
146
+ "qwen2.5-coder:14b": ModelSpec(
147
+ id="qwen2.5-coder:14b",
148
+ name="Qwen 2.5 Coder 14B",
149
+ provider=ModelProvider.OLLAMA,
150
+ context_window=32768,
151
+ is_local=True,
152
+ description="High-accuracy local code generation and refactoring",
153
+ ),
154
+ "deepseek-r1:7b": ModelSpec(
155
+ id="deepseek-r1:7b",
156
+ name="DeepSeek R1 Distill Qwen 7B",
157
+ provider=ModelProvider.OLLAMA,
158
+ context_window=32768,
159
+ is_local=True,
160
+ supports_thinking=True,
161
+ description="Local reasoning model with deep chain-of-thought",
162
+ ),
163
+ "deepseek-r1:14b": ModelSpec(
164
+ id="deepseek-r1:14b",
165
+ name="DeepSeek R1 Distill Qwen 14B",
166
+ provider=ModelProvider.OLLAMA,
167
+ context_window=32768,
168
+ is_local=True,
169
+ supports_thinking=True,
170
+ description="Advanced local reasoning and architectural planning",
171
+ ),
172
+ "llama3.3:70b": ModelSpec(
173
+ id="llama3.3:70b",
174
+ name="Meta Llama 3.3 70B",
175
+ provider=ModelProvider.OLLAMA,
176
+ context_window=128000,
177
+ is_local=True,
178
+ description="State-of-the-art open weights flagship model",
179
+ ),
180
+ "codellama:7b": ModelSpec(
181
+ id="codellama:7b",
182
+ name="CodeLlama 7B",
183
+ provider=ModelProvider.OLLAMA,
184
+ context_window=16384,
185
+ is_local=True,
186
+ description="Meta's classic code-specialized model",
187
+ ),
188
+ "phi4:14b": ModelSpec(
189
+ id="phi4:14b",
190
+ name="Microsoft Phi-4 14B",
191
+ provider=ModelProvider.OLLAMA,
192
+ context_window=16384,
193
+ is_local=True,
194
+ description="High-reasoning compact model from Microsoft Research",
195
+ ),
196
+
197
+ # --- Google Gemini ---
198
+ "gemini-2.0-flash": ModelSpec(
199
+ id="gemini-2.0-flash",
200
+ name="Google Gemini 2.0 Flash",
201
+ provider=ModelProvider.GEMINI,
202
+ context_window=1048576,
203
+ max_output_tokens=8192,
204
+ prompt_price_per_m=0.10,
205
+ completion_price_per_m=0.40,
206
+ supports_tools=True,
207
+ supports_vision=True,
208
+ supports_thinking=True,
209
+ env_var_key="GEMINI_API_KEY",
210
+ description="Next-generation ultra-fast multimodal model with 1M context",
211
+ ),
212
+ "gemini-1.5-pro": ModelSpec(
213
+ id="gemini-1.5-pro",
214
+ name="Google Gemini 1.5 Pro",
215
+ provider=ModelProvider.GEMINI,
216
+ context_window=2097152,
217
+ max_output_tokens=8192,
218
+ prompt_price_per_m=1.25,
219
+ completion_price_per_m=5.00,
220
+ supports_tools=True,
221
+ supports_vision=True,
222
+ env_var_key="GEMINI_API_KEY",
223
+ description="2M token context window for full-codebase reasoning",
224
+ ),
225
+
226
+ # --- Anthropic Claude ---
227
+ "claude-3-7-sonnet": ModelSpec(
228
+ id="claude-3-7-sonnet",
229
+ name="Claude 3.7 Sonnet (Hybrid Thinking)",
230
+ provider=ModelProvider.ANTHROPIC,
231
+ context_window=200000,
232
+ max_output_tokens=8192,
233
+ prompt_price_per_m=3.00,
234
+ completion_price_per_m=15.00,
235
+ supports_tools=True,
236
+ supports_vision=True,
237
+ supports_thinking=True,
238
+ env_var_key="ANTHROPIC_API_KEY",
239
+ description="Anthropic's flagship coding and hybrid reasoning model",
240
+ ),
241
+ "claude-3-5-sonnet": ModelSpec(
242
+ id="claude-3-5-sonnet",
243
+ name="Claude 3.5 Sonnet",
244
+ provider=ModelProvider.ANTHROPIC,
245
+ context_window=200000,
246
+ max_output_tokens=8192,
247
+ prompt_price_per_m=3.00,
248
+ completion_price_per_m=15.00,
249
+ supports_tools=True,
250
+ supports_vision=True,
251
+ env_var_key="ANTHROPIC_API_KEY",
252
+ description="Industry-standard benchmark leader in code generation",
253
+ ),
254
+ "claude-3-5-haiku": ModelSpec(
255
+ id="claude-3-5-haiku",
256
+ name="Claude 3.5 Haiku",
257
+ provider=ModelProvider.ANTHROPIC,
258
+ context_window=200000,
259
+ max_output_tokens=4096,
260
+ prompt_price_per_m=0.80,
261
+ completion_price_per_m=4.00,
262
+ supports_tools=True,
263
+ env_var_key="ANTHROPIC_API_KEY",
264
+ description="High-speed, cost-effective coding model",
265
+ ),
266
+
267
+ # --- OpenAI ---
268
+ "gpt-4o": ModelSpec(
269
+ id="gpt-4o",
270
+ name="OpenAI GPT-4o",
271
+ provider=ModelProvider.OPENAI,
272
+ context_window=128000,
273
+ max_output_tokens=4096,
274
+ prompt_price_per_m=2.50,
275
+ completion_price_per_m=10.00,
276
+ supports_tools=True,
277
+ supports_vision=True,
278
+ env_var_key="OPENAI_API_KEY",
279
+ description="Omni flagship model from OpenAI",
280
+ ),
281
+ "gpt-4o-mini": ModelSpec(
282
+ id="gpt-4o-mini",
283
+ name="OpenAI GPT-4o Mini",
284
+ provider=ModelProvider.OPENAI,
285
+ context_window=128000,
286
+ max_output_tokens=4096,
287
+ prompt_price_per_m=0.15,
288
+ completion_price_per_m=0.60,
289
+ supports_tools=True,
290
+ env_var_key="OPENAI_API_KEY",
291
+ description="Affordable fast model for everyday coding tasks",
292
+ ),
293
+ "o3-mini": ModelSpec(
294
+ id="o3-mini",
295
+ name="OpenAI o3-mini",
296
+ provider=ModelProvider.OPENAI,
297
+ context_window=200000,
298
+ max_output_tokens=8192,
299
+ prompt_price_per_m=1.10,
300
+ completion_price_per_m=4.40,
301
+ supports_tools=True,
302
+ supports_thinking=True,
303
+ env_var_key="OPENAI_API_KEY",
304
+ description="Advanced STEM & coding reasoning model with reasoning tiers",
305
+ ),
306
+
307
+ # --- DeepSeek ---
308
+ "deepseek-chat": ModelSpec(
309
+ id="deepseek-chat",
310
+ name="DeepSeek V3",
311
+ provider=ModelProvider.DEEPSEEK,
312
+ context_window=64000,
313
+ max_output_tokens=8192,
314
+ prompt_price_per_m=0.27,
315
+ completion_price_per_m=1.10,
316
+ supports_tools=True,
317
+ base_url="https://api.deepseek.com/v1",
318
+ env_var_key="DEEPSEEK_API_KEY",
319
+ description="High-performance MoE coding & general model",
320
+ ),
321
+ "deepseek-reasoner": ModelSpec(
322
+ id="deepseek-reasoner",
323
+ name="DeepSeek R1 Reasoning",
324
+ provider=ModelProvider.DEEPSEEK,
325
+ context_window=64000,
326
+ max_output_tokens=8192,
327
+ prompt_price_per_m=0.55,
328
+ completion_price_per_m=2.19,
329
+ supports_thinking=True,
330
+ base_url="https://api.deepseek.com/v1",
331
+ env_var_key="DEEPSEEK_API_KEY",
332
+ description="DeepSeek R1 full reasoning with verified think trace",
333
+ ),
334
+
335
+ # --- Groq (Ultra-Fast) ---
336
+ "llama-3.3-70b-versatile": ModelSpec(
337
+ id="llama-3.3-70b-versatile",
338
+ name="Groq Llama 3.3 70B (300+ tok/s)",
339
+ provider=ModelProvider.GROQ,
340
+ context_window=128000,
341
+ max_output_tokens=8192,
342
+ prompt_price_per_m=0.59,
343
+ completion_price_per_m=0.79,
344
+ supports_tools=True,
345
+ base_url="https://api.groq.com/openai/v1",
346
+ env_var_key="GROQ_API_KEY",
347
+ description="Ultra-low-latency Llama 3.3 powered by Groq LPUs",
348
+ ),
349
+ "qwen-2.5-coder-32b": ModelSpec(
350
+ id="qwen-2.5-coder-32b",
351
+ name="Groq Qwen 2.5 Coder 32B",
352
+ provider=ModelProvider.GROQ,
353
+ context_window=128000,
354
+ max_output_tokens=8192,
355
+ supports_tools=True,
356
+ base_url="https://api.groq.com/openai/v1",
357
+ env_var_key="GROQ_API_KEY",
358
+ description="Ultra-fast code generation on Groq LPU hardware",
359
+ ),
360
+
361
+ # --- Mistral AI ---
362
+ "codestral-latest": ModelSpec(
363
+ id="codestral-latest",
364
+ name="Mistral Codestral",
365
+ provider=ModelProvider.MISTRAL,
366
+ context_window=32768,
367
+ max_output_tokens=8192,
368
+ prompt_price_per_m=0.30,
369
+ completion_price_per_m=0.90,
370
+ supports_tools=True,
371
+ base_url="https://api.mistral.ai/v1",
372
+ env_var_key="MISTRAL_API_KEY",
373
+ description="Mistral's purpose-built code generation & fill-in-middle model",
374
+ ),
375
+
376
+ # --- OpenRouter ---
377
+ "openrouter/auto": ModelSpec(
378
+ id="openrouter/auto",
379
+ name="OpenRouter Smart Auto-Router",
380
+ provider=ModelProvider.OPENROUTER,
381
+ context_window=128000,
382
+ supports_tools=True,
383
+ base_url="https://openrouter.ai/api/v1",
384
+ env_var_key="OPENROUTER_API_KEY",
385
+ description="Universal router picking optimal model per prompt",
386
+ ),
387
+ }
388
+
389
+
390
+ class ModelHub:
391
+ """
392
+ Universal Model Hub & Provider Manager for K-CLI.
393
+ Discovers local models, tests connectivity, manages API keys,
394
+ and constructs multi-tier fallback cascades.
395
+ """
396
+
397
+ def __init__(
398
+ self,
399
+ ollama_url: str = "http://localhost:11434",
400
+ llamacpp_url: str = "http://localhost:8080",
401
+ config_file: Optional[str] = None,
402
+ ):
403
+ self.ollama_url = ollama_url.rstrip("/")
404
+ self.llamacpp_url = llamacpp_url.rstrip("/")
405
+ self.config_file = config_file or str(Path.home() / ".kcli" / "models.json")
406
+ self.registry: Dict[str, ModelSpec] = dict(MODEL_CATALOG_REGISTRY)
407
+ self.custom_models: Dict[str, ModelSpec] = {}
408
+ self._load_custom_config()
409
+
410
+ def _load_custom_config(self) -> None:
411
+ """Loads user-registered custom models and local endpoints from JSON."""
412
+ cfg_path = Path(self.config_file)
413
+ if cfg_path.exists():
414
+ try:
415
+ data = json.loads(cfg_path.read_text(encoding="utf-8"))
416
+ for item in data.get("models", []):
417
+ spec = ModelSpec(
418
+ id=item["id"],
419
+ name=item.get("name", item["id"]),
420
+ provider=ModelProvider(item.get("provider", "openai-compatible")),
421
+ context_window=item.get("context_window", 32768),
422
+ is_local=item.get("is_local", False),
423
+ base_url=item.get("base_url"),
424
+ env_var_key=item.get("env_var_key"),
425
+ )
426
+ self.custom_models[spec.id] = spec
427
+ self.registry[spec.id] = spec
428
+ except Exception as exc:
429
+ logger.warning(f"Failed loading custom model config: {exc}")
430
+
431
+ def save_custom_config(self) -> bool:
432
+ """Persists custom model definitions to disk."""
433
+ cfg_path = Path(self.config_file)
434
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
435
+ try:
436
+ data = {"models": [m.to_dict() for m in self.custom_models.values()]}
437
+ cfg_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
438
+ return True
439
+ except Exception as exc:
440
+ logger.error(f"Failed saving custom models: {exc}")
441
+ return False
442
+
443
+ def register_model(self, spec: ModelSpec) -> None:
444
+ """Registers a custom or self-hosted model in the registry."""
445
+ self.custom_models[spec.id] = spec
446
+ self.registry[spec.id] = spec
447
+ self.save_custom_config()
448
+
449
+ def resolve_model(self, identifier: str) -> Optional[ModelSpec]:
450
+ """
451
+ Parses model identifier (e.g. `gemini-2.0-flash`, `ollama/qwen2.5-coder:7b`,
452
+ `deepseek/deepseek-r1`, `groq/llama-3.3-70b-versatile`) and resolves ModelSpec.
453
+ """
454
+ if not identifier:
455
+ return self.registry.get("qwen2.5-coder:1.5b")
456
+
457
+ clean_id = identifier.strip().lower()
458
+
459
+ # 1. Exact match in registry
460
+ if clean_id in self.registry:
461
+ return self.registry[clean_id]
462
+
463
+ # 2. Check with provider prefix stripped
464
+ if "/" in clean_id:
465
+ provider_prefix, model_name = clean_id.split("/", 1)
466
+ if model_name in self.registry:
467
+ return self.registry[model_name]
468
+
469
+ # Dynamic provider instantiation
470
+ prov_enum = ModelProvider.OPENAI
471
+ for p in ModelProvider:
472
+ if p.value == provider_prefix:
473
+ prov_enum = p
474
+ break
475
+
476
+ return ModelSpec(
477
+ id=clean_id,
478
+ name=model_name,
479
+ provider=prov_enum,
480
+ is_local=(prov_enum in (ModelProvider.OLLAMA, ModelProvider.LLAMACPP, ModelProvider.NATIVE)),
481
+ )
482
+
483
+ # 3. Fuzzy matching against catalog
484
+ for spec_id, spec in self.registry.items():
485
+ if clean_id in spec_id.lower() or clean_id in spec.name.lower():
486
+ return spec
487
+
488
+ # 4. Fallback: treat as Ollama local model
489
+ return ModelSpec(
490
+ id=identifier,
491
+ name=identifier,
492
+ provider=ModelProvider.OLLAMA,
493
+ is_local=True,
494
+ )
495
+
496
+ def discover_local_ollama_models(self) -> List[Dict[str, Any]]:
497
+ """Queries local Ollama daemon dynamically for ALL installed models and metadata."""
498
+ try:
499
+ req = urllib.request.Request(
500
+ f"{self.ollama_url}/api/tags",
501
+ headers={"User-Agent": "K-CLI/1.0.0 (AGY Edition)"},
502
+ )
503
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
504
+ data = json.loads(resp.read().decode("utf-8"))
505
+ discovered: List[Dict[str, Any]] = []
506
+ for m in data.get("models", []):
507
+ name = m.get("name", "")
508
+ if name:
509
+ size_gb = round(m.get("size", 0) / (1024**3), 2)
510
+ details = m.get("details", {})
511
+ quant = details.get("quantization_level", "")
512
+ param_size = details.get("parameter_size", "")
513
+ family = details.get("family", "")
514
+
515
+ spec = ModelSpec(
516
+ id=name,
517
+ name=f"{name} ({param_size} {quant})".strip(),
518
+ provider=ModelProvider.OLLAMA,
519
+ context_window=32768,
520
+ is_local=True,
521
+ is_installed=True,
522
+ description=f"Local Ollama model: {family} {param_size} {quant} ({size_gb} GB)",
523
+ )
524
+ self.registry[name] = spec
525
+ discovered.append({
526
+ "name": name,
527
+ "size_gb": size_gb,
528
+ "quant": quant,
529
+ "param_size": param_size,
530
+ "family": family,
531
+ "spec": spec,
532
+ })
533
+ return discovered
534
+ except Exception:
535
+ return []
536
+
537
+ def discover_all_live_models(self) -> List[ModelSpec]:
538
+ """
539
+ Dynamically queries all active local daemons (Ollama, LM Studio, vLLM)
540
+ and cloud provider endpoints to discover every available model in real time.
541
+ """
542
+ # 1. Local Ollama
543
+ self.discover_local_ollama_models()
544
+
545
+ # 2. Local LM Studio / vLLM / OpenAI Compatible endpoints
546
+ for local_url in ("http://localhost:1234/v1", "http://localhost:8000/v1", "http://localhost:8080/v1"):
547
+ try:
548
+ req = urllib.request.Request(f"{local_url}/models", headers={"User-Agent": "K-CLI"})
549
+ with urllib.request.urlopen(req, timeout=1.5) as resp:
550
+ if resp.status == 200:
551
+ data = json.loads(resp.read().decode("utf-8"))
552
+ for item in data.get("data", []):
553
+ m_id = item.get("id")
554
+ if m_id:
555
+ spec = ModelSpec(
556
+ id=f"local/{m_id}",
557
+ name=f"Local ({local_url}): {m_id}",
558
+ provider=ModelProvider.OPENAI_COMPATIBLE,
559
+ is_local=True,
560
+ base_url=local_url,
561
+ description=f"Local self-hosted model running on {local_url}",
562
+ )
563
+ self.registry[spec.id] = spec
564
+ except Exception:
565
+ pass
566
+
567
+ # 3. Groq Dynamic Models
568
+ groq_key = os.environ.get("GROQ_API_KEY")
569
+ if groq_key:
570
+ try:
571
+ req = urllib.request.Request(
572
+ "https://api.groq.com/openai/v1/models",
573
+ headers={"Authorization": f"Bearer {groq_key}", "User-Agent": "K-CLI"},
574
+ )
575
+ with urllib.request.urlopen(req, timeout=2.5) as resp:
576
+ if resp.status == 200:
577
+ data = json.loads(resp.read().decode("utf-8"))
578
+ for item in data.get("data", []):
579
+ m_id = item.get("id")
580
+ if m_id and ("llama" in m_id or "qwen" in m_id or "deepseek" in m_id or "mixtral" in m_id):
581
+ spec = ModelSpec(
582
+ id=f"groq/{m_id}",
583
+ name=f"Groq Fast: {m_id}",
584
+ provider=ModelProvider.GROQ,
585
+ base_url="https://api.groq.com/openai/v1",
586
+ env_var_key="GROQ_API_KEY",
587
+ description=f"Groq ultra-fast LPU inference: {m_id}",
588
+ )
589
+ self.registry[spec.id] = spec
590
+ except Exception:
591
+ pass
592
+
593
+ # 4. Google Gemini Dynamic Models
594
+ gemini_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
595
+ if gemini_key:
596
+ try:
597
+ req = urllib.request.Request(
598
+ f"https://generativelanguage.googleapis.com/v1beta/models?key={gemini_key}",
599
+ headers={"User-Agent": "K-CLI"},
600
+ )
601
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
602
+ if resp.status == 200:
603
+ data = json.loads(resp.read().decode("utf-8"))
604
+ for item in data.get("models", []):
605
+ raw_name = item.get("name", "").replace("models/", "")
606
+ if "gemini" in raw_name and "deprecated" not in raw_name:
607
+ spec = ModelSpec(
608
+ id=raw_name,
609
+ name=f"Google Gemini: {raw_name}",
610
+ provider=ModelProvider.GEMINI,
611
+ env_var_key="GEMINI_API_KEY",
612
+ description=item.get("description", f"Google Gemini model {raw_name}")[:60],
613
+ )
614
+ self.registry[spec.id] = spec
615
+ except Exception:
616
+ pass
617
+
618
+ # 5. DeepSeek Dynamic Models
619
+ deepseek_key = os.environ.get("DEEPSEEK_API_KEY")
620
+ if deepseek_key:
621
+ try:
622
+ req = urllib.request.Request(
623
+ "https://api.deepseek.com/models",
624
+ headers={"Authorization": f"Bearer {deepseek_key}", "User-Agent": "K-CLI"},
625
+ )
626
+ with urllib.request.urlopen(req, timeout=2.5) as resp:
627
+ if resp.status == 200:
628
+ data = json.loads(resp.read().decode("utf-8"))
629
+ for item in data.get("data", []):
630
+ m_id = item.get("id")
631
+ if m_id:
632
+ spec = ModelSpec(
633
+ id=f"deepseek/{m_id}",
634
+ name=f"DeepSeek: {m_id}",
635
+ provider=ModelProvider.DEEPSEEK,
636
+ base_url="https://api.deepseek.com",
637
+ env_var_key="DEEPSEEK_API_KEY",
638
+ description=f"DeepSeek Reasoning & Coding model {m_id}",
639
+ )
640
+ self.registry[spec.id] = spec
641
+ except Exception:
642
+ pass
643
+
644
+ # 6. OpenAI Dynamic Models
645
+ openai_key = os.environ.get("OPENAI_API_KEY")
646
+ if openai_key:
647
+ try:
648
+ req = urllib.request.Request(
649
+ "https://api.openai.com/v1/models",
650
+ headers={"Authorization": f"Bearer {openai_key}", "User-Agent": "K-CLI"},
651
+ )
652
+ with urllib.request.urlopen(req, timeout=2.5) as resp:
653
+ if resp.status == 200:
654
+ data = json.loads(resp.read().decode("utf-8"))
655
+ for item in data.get("data", []):
656
+ m_id = item.get("id", "")
657
+ if m_id.startswith("gpt-4") or m_id.startswith("o1") or m_id.startswith("o3") or m_id.startswith("chatgpt"):
658
+ spec = ModelSpec(
659
+ id=f"openai/{m_id}",
660
+ name=f"OpenAI: {m_id}",
661
+ provider=ModelProvider.OPENAI,
662
+ base_url="https://api.openai.com/v1",
663
+ env_var_key="OPENAI_API_KEY",
664
+ description=f"OpenAI model {m_id}",
665
+ )
666
+ self.registry[spec.id] = spec
667
+ except Exception:
668
+ pass
669
+
670
+ # 7. Anthropic Dynamic Models
671
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
672
+ if anthropic_key:
673
+ try:
674
+ req = urllib.request.Request(
675
+ "https://api.anthropic.com/v1/models",
676
+ headers={
677
+ "x-api-key": anthropic_key,
678
+ "anthropic-version": "2023-06-01",
679
+ "User-Agent": "K-CLI",
680
+ },
681
+ )
682
+ with urllib.request.urlopen(req, timeout=2.5) as resp:
683
+ if resp.status == 200:
684
+ data = json.loads(resp.read().decode("utf-8"))
685
+ for item in data.get("data", []):
686
+ m_id = item.get("id", "")
687
+ if m_id:
688
+ spec = ModelSpec(
689
+ id=f"anthropic/{m_id}",
690
+ name=f"Anthropic: {m_id}",
691
+ provider=ModelProvider.ANTHROPIC,
692
+ env_var_key="ANTHROPIC_API_KEY",
693
+ description=item.get("display_name", f"Anthropic model {m_id}"),
694
+ )
695
+ self.registry[spec.id] = spec
696
+ except Exception:
697
+ for m_id in ("claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"):
698
+ spec = ModelSpec(
699
+ id=f"anthropic/{m_id}",
700
+ name=f"Anthropic: {m_id}",
701
+ provider=ModelProvider.ANTHROPIC,
702
+ env_var_key="ANTHROPIC_API_KEY",
703
+ description=f"Anthropic state-of-the-art model {m_id}",
704
+ )
705
+ self.registry[spec.id] = spec
706
+
707
+ # 8. OpenRouter Dynamic Models
708
+ openrouter_key = os.environ.get("OPENROUTER_API_KEY")
709
+ if openrouter_key:
710
+ try:
711
+ req = urllib.request.Request(
712
+ "https://openrouter.ai/api/v1/models",
713
+ headers={"Authorization": f"Bearer {openrouter_key}", "User-Agent": "K-CLI"},
714
+ )
715
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
716
+ if resp.status == 200:
717
+ data = json.loads(resp.read().decode("utf-8"))
718
+ for item in data.get("data", [])[:20]:
719
+ m_id = item.get("id", "")
720
+ if m_id:
721
+ spec = ModelSpec(
722
+ id=f"openrouter/{m_id}",
723
+ name=f"OpenRouter: {m_id}",
724
+ provider=ModelProvider.OPENROUTER,
725
+ base_url="https://openrouter.ai/api/v1",
726
+ env_var_key="OPENROUTER_API_KEY",
727
+ description=item.get("description", f"OpenRouter model {m_id}")[:60],
728
+ )
729
+ self.registry[spec.id] = spec
730
+ except Exception:
731
+ pass
732
+
733
+ # 9. AWS Bedrock Models
734
+ if os.environ.get("AWS_ACCESS_KEY_ID") or os.environ.get("BEDROCK_MODEL_ID"):
735
+ for m_id, label in [
736
+ ("anthropic.claude-3-5-sonnet-20241022-v2:0", "Bedrock Claude 3.5 Sonnet v2"),
737
+ ("amazon.nova-pro-v1:0", "Bedrock Amazon Nova Pro"),
738
+ ]:
739
+ spec = ModelSpec(
740
+ id=m_id,
741
+ name=f"AWS Bedrock: {label}",
742
+ provider=ModelProvider.BEDROCK,
743
+ description=f"AWS Bedrock Foundation Model {m_id}",
744
+ )
745
+ self.registry[spec.id] = spec
746
+
747
+ return list(self.registry.values())
748
+
749
+ def get_verified_active_models(self) -> List[ModelSpec]:
750
+ """Returns only models whose provider is actively configured and reachable."""
751
+ self.discover_all_live_models()
752
+ active = []
753
+ for spec in self.registry.values():
754
+ if spec.is_local:
755
+ # If local, check if installed in ollama or custom local server
756
+ if spec.is_installed or spec.base_url:
757
+ active.append(spec)
758
+ else:
759
+ if self.is_provider_configured(spec.provider):
760
+ active.append(spec)
761
+ return active
762
+
763
+ def is_provider_configured(self, provider: ModelProvider) -> bool:
764
+ """Checks if a provider has active API credentials or local service available."""
765
+ if provider == ModelProvider.OLLAMA:
766
+ try:
767
+ req = urllib.request.Request(f"{self.ollama_url}/api/tags", headers={"User-Agent": "K-CLI/1.0.0"})
768
+ with urllib.request.urlopen(req, timeout=1.0) as resp:
769
+ return resp.status == 200
770
+ except Exception:
771
+ return False
772
+ elif provider == ModelProvider.LLAMACPP:
773
+ try:
774
+ req = urllib.request.Request(f"{self.llamacpp_url}/v1/models", headers={"User-Agent": "K-CLI/1.0.0"})
775
+ with urllib.request.urlopen(req, timeout=1.0) as resp:
776
+ return resp.status == 200
777
+ except Exception:
778
+ return False
779
+ elif provider == ModelProvider.GEMINI:
780
+ return bool(os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"))
781
+ elif provider == ModelProvider.ANTHROPIC:
782
+ return bool(os.environ.get("ANTHROPIC_API_KEY"))
783
+ elif provider == ModelProvider.OPENAI:
784
+ return bool(os.environ.get("OPENAI_API_KEY"))
785
+ elif provider == ModelProvider.DEEPSEEK:
786
+ return bool(os.environ.get("DEEPSEEK_API_KEY"))
787
+ elif provider == ModelProvider.GROQ:
788
+ return bool(os.environ.get("GROQ_API_KEY"))
789
+ elif provider == ModelProvider.MISTRAL:
790
+ return bool(os.environ.get("MISTRAL_API_KEY"))
791
+ elif provider == ModelProvider.OPENROUTER:
792
+ return bool(os.environ.get("OPENROUTER_API_KEY"))
793
+ elif provider == ModelProvider.MOCK:
794
+ return True
795
+ return False
796
+
797
+ def list_models(
798
+ self,
799
+ provider: Optional[Union[ModelProvider, str]] = None,
800
+ local_only: bool = False,
801
+ ) -> List[ModelSpec]:
802
+ """Returns list of models matching optional provider or local filters."""
803
+ ollama_models = self.discover_local_ollama_models()
804
+ ollama_installed = {m["name"] for m in ollama_models}
805
+ results: List[ModelSpec] = []
806
+
807
+ prov_val = provider.value if isinstance(provider, ModelProvider) else (provider.lower() if provider else None)
808
+
809
+ for spec in self.registry.values():
810
+ if local_only and not spec.is_local:
811
+ continue
812
+ if prov_val and spec.provider.value != prov_val:
813
+ continue
814
+
815
+ # Update installed flag for Ollama models
816
+ if spec.provider == ModelProvider.OLLAMA:
817
+ spec.is_installed = (spec.id in ollama_installed or f"{spec.id}:latest" in ollama_installed)
818
+
819
+ results.append(spec)
820
+
821
+ return results
822
+
823
+
824
+ def pull_model(
825
+ self,
826
+ model_name: str,
827
+ stream_callback: Optional[Callable[[str], None]] = None,
828
+ ) -> bool:
829
+ """Pulls a local model via Ollama daemon API."""
830
+ try:
831
+ payload = json.dumps({"name": model_name, "stream": True}).encode("utf-8")
832
+ req = urllib.request.Request(
833
+ f"{self.ollama_url}/api/pull",
834
+ data=payload,
835
+ headers={"Content-Type": "application/json", "User-Agent": "K-CLI/1.0.0"},
836
+ method="POST",
837
+ )
838
+ with urllib.request.urlopen(req, timeout=300.0) as resp:
839
+ for line in resp:
840
+ if not line:
841
+ continue
842
+ try:
843
+ chunk = json.loads(line.decode("utf-8"))
844
+ status = chunk.get("status", "")
845
+ completed = chunk.get("completed", 0)
846
+ total = chunk.get("total", 0)
847
+ if total > 0 and stream_callback:
848
+ pct = (completed / total) * 100.0
849
+ stream_callback(f"{status}: {pct:.1f}%\n")
850
+ elif stream_callback and status:
851
+ stream_callback(f"{status}\n")
852
+ except Exception:
853
+ pass
854
+ return True
855
+ except Exception as exc:
856
+ logger.error(f"Failed pulling model {model_name}: {exc}")
857
+ return False
858
+
859
+ def benchmark_model(
860
+ self,
861
+ model_name: str,
862
+ prompt: str = "Write a Python function to compute fibonacci numbers iteratively.",
863
+ driver: Optional[Any] = None,
864
+ ) -> ModelBenchmarkResult:
865
+ """
866
+ Executes benchmark test on model to calculate Time-to-First-Token,
867
+ throughput tokens/second, memory RSS footprint, and output correctness.
868
+ """
869
+ from k_cli.core.llm_driver import LLMDriver
870
+
871
+ start_time = time.time()
872
+ first_token_time: Optional[float] = None
873
+ token_count = 0
874
+ collected_chunks: List[str] = []
875
+
876
+ def benchmark_stream(chunk: str) -> None:
877
+ nonlocal first_token_time, token_count
878
+ if first_token_time is None:
879
+ first_token_time = time.time()
880
+ token_count += 1
881
+ collected_chunks.append(chunk)
882
+
883
+ spec = self.resolve_model(model_name)
884
+ active_driver = driver or LLMDriver(model_name=spec.id if spec else model_name, mock_mode=False)
885
+
886
+ try:
887
+ res = active_driver.generate(prompt=prompt, stream_callback=benchmark_stream)
888
+ duration = time.time() - start_time
889
+ ttft = (first_token_time - start_time) if first_token_time else duration
890
+ tok_per_sec = (token_count / duration) if duration > 0 else 0.0
891
+
892
+ import psutil
893
+ ram_mb = psutil.Process().memory_info().rss / (1024 * 1024)
894
+
895
+ return ModelBenchmarkResult(
896
+ model_id=spec.id if spec else model_name,
897
+ provider=spec.provider.value if spec else "unknown",
898
+ success=True,
899
+ tokens_generated=token_count or len(res.split()),
900
+ duration_seconds=duration,
901
+ tokens_per_second=tok_per_sec or (len(res.split()) / max(duration, 0.001)),
902
+ time_to_first_token=ttft,
903
+ ram_rss_mb=ram_mb,
904
+ sample_output=res[:200],
905
+ )
906
+ except Exception as exc:
907
+ return ModelBenchmarkResult(
908
+ model_id=spec.id if spec else model_name,
909
+ provider=spec.provider.value if spec else "unknown",
910
+ success=False,
911
+ duration_seconds=time.time() - start_time,
912
+ error_message=str(exc),
913
+ )