alpiecode 9.0.6__tar.gz → 10.0.1__tar.gz

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 (53) hide show
  1. {alpiecode-9.0.6 → alpiecode-10.0.1}/PKG-INFO +2 -2
  2. {alpiecode-9.0.6 → alpiecode-10.0.1}/README.md +1 -1
  3. {alpiecode-9.0.6 → alpiecode-10.0.1}/pyproject.toml +2 -2
  4. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/PKG-INFO +2 -2
  5. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/__init__.py +2 -1
  6. alpiecode-10.0.1/src/codeagent/backends/local_backend.py +314 -0
  7. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/config.py +4 -0
  8. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/discovery.py +6 -1
  9. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/doctor.py +72 -32
  10. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/local_model.py +16 -0
  11. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/orchestrator.py +90 -13
  12. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/server.py +5 -6
  13. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/tools.py +6 -2
  14. alpiecode-10.0.1/test/test_offline_mode.py +314 -0
  15. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase1.py +42 -0
  16. alpiecode-9.0.6/src/codeagent/backends/local_backend.py +0 -167
  17. alpiecode-9.0.6/test/test_offline_mode.py +0 -78
  18. {alpiecode-9.0.6 → alpiecode-10.0.1}/setup.cfg +0 -0
  19. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode/__init__.py +0 -0
  20. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  21. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  22. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  23. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
  24. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  25. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/agent.py +0 -0
  26. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/__init__.py +0 -0
  27. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/base.py +0 -0
  28. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/openai_backend.py +0 -0
  29. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/cache.py +0 -0
  30. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/cli.py +0 -0
  31. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/client.py +0 -0
  32. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/compaction.py +0 -0
  33. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/context.py +0 -0
  34. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/executor.py +0 -0
  35. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  36. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/git_ops.py +0 -0
  37. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/github.py +0 -0
  38. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/guardian.py +0 -0
  39. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/guardrails.py +0 -0
  40. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/ipython_ext.py +0 -0
  41. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/media.py +0 -0
  42. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/memory.py +0 -0
  43. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/progress.py +0 -0
  44. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/prompt.py +0 -0
  45. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/rephraser.py +0 -0
  46. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/session.py +0 -0
  47. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/updater.py +0 -0
  48. {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/vscode_installer.py +0 -0
  49. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_audit_fixes.py +0 -0
  50. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_chat_mode.py +0 -0
  51. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_claude_codex_capabilities.py +0 -0
  52. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase2.py +0 -0
  53. {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase3.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 9.0.6
4
- Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
3
+ Version: 10.0.1
4
+ Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
7
7
  Requires-Dist: rich>=13.0
@@ -1,4 +1,4 @@
1
- # ⚡ AlpieCode: Autonomous AI Software Engineering Agent & Full Developer Platform
1
+ # AlpieCode
2
2
 
3
3
  [![Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
4
4
  [![Version](https://img.shields.io/badge/alpiecode-v9.0.1-emerald.svg)](https://github.com/169Pi/codeagent-poc)
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "9.0.6"
8
- description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
7
+ version = "10.0.1"
8
+ description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
11
11
  "huggingface_hub>=0.20.0",
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 9.0.6
4
- Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
3
+ Version: 10.0.1
4
+ Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
7
7
  Requires-Dist: rich>=13.0
@@ -1,6 +1,7 @@
1
1
  import warnings
2
2
  warnings.filterwarnings("ignore", category=DeprecationWarning)
3
3
  warnings.filterwarnings("ignore", category=FutureWarning)
4
+ warnings.filterwarnings("ignore", category=SyntaxWarning, message=r".*invalid escape sequence.*")
4
5
  warnings.filterwarnings("ignore", module="jupyter_client.*")
5
6
 
6
7
  """
@@ -16,7 +17,7 @@ Supports:
16
17
  from pathlib import Path
17
18
  from typing import Optional, Any, Dict
18
19
 
19
- __version__ = "9.0.6"
20
+ __version__ = "10.0.1"
20
21
 
21
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
22
23
  """Run an autonomous coding task programmatically."""
@@ -0,0 +1,314 @@
1
+ """
2
+ Local GGUF model inference backend.
3
+ """
4
+
5
+ from typing import Any, List, Optional
6
+
7
+ from ..config import Config
8
+ from ..local_model import LocalModel
9
+ from .base import ChatResponse, ToolCall
10
+
11
+
12
+ class LocalBackend:
13
+ """Backend for local GGUF model inference via llama-cpp-python."""
14
+
15
+ def __init__(self, cfg: Config):
16
+ self._cfg = cfg
17
+ self._model: Optional[LocalModel] = None
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ return f"Local GGUF ({self._cfg.model_repo})"
22
+
23
+ @property
24
+ def is_available(self) -> bool:
25
+ return True # Local model is always available (downloads if missing)
26
+
27
+ @property
28
+ def context_window(self) -> int:
29
+ return self._cfg.n_ctx
30
+
31
+ def _ensure_model(self) -> LocalModel:
32
+ if self._model is None:
33
+ self._model = LocalModel(
34
+ repo_id=self._cfg.model_repo,
35
+ n_ctx=self._cfg.n_ctx,
36
+ n_gpu_layers=self._cfg.n_gpu_layers,
37
+ token=self._cfg.hf_token,
38
+ )
39
+ return self._model
40
+
41
+ def load_model(self) -> None:
42
+ """Explicitly load model into VRAM/RAM (warmup)."""
43
+ model = self._ensure_model()
44
+ model.load()
45
+
46
+ @staticmethod
47
+ def _sanitize_messages_for_gguf(messages: List[dict]) -> List[dict]:
48
+ """
49
+ Sanitize OpenAI-format messages for GGUF Jinja2 chat templates.
50
+ Fixes 'Can only get item pairs from a mapping' errors by:
51
+ - Ensuring content is always a string (never None or list)
52
+ - Stripping tool_calls and tool messages (GGUF templates don't support them)
53
+ - Removing tool-related role messages
54
+ """
55
+ sanitized = []
56
+ for msg in messages:
57
+ role = msg.get("role", "")
58
+ if role == "tool":
59
+ # Bridge tool execution results into ChatML user turns so local model sees outputs
60
+ name = msg.get("name") or "tool"
61
+ raw_content = msg.get("content") or ""
62
+ if not isinstance(raw_content, str):
63
+ raw_content = str(raw_content)
64
+ # Smart failure-preserving truncation for local offline context
65
+ from ..tools import _smart_truncate
66
+ raw_content = _smart_truncate(raw_content, max_chars=1800)
67
+
68
+ tool_text = f"[Tool Result for {name}]:\n{raw_content}"
69
+ if sanitized and sanitized[-1].get("role") == "user":
70
+ sanitized[-1]["content"] += "\n\n" + tool_text
71
+ else:
72
+ sanitized.append({"role": "user", "content": tool_text})
73
+ continue
74
+
75
+ clean = {"role": role}
76
+
77
+ # Ensure content is always a string
78
+ content = msg.get("content")
79
+ if content is None:
80
+ content = ""
81
+ elif isinstance(content, list):
82
+ # Multimodal content (image_url + text blocks) — extract text parts
83
+ text_parts = []
84
+ for part in content:
85
+ if isinstance(part, dict):
86
+ if part.get("type") == "text":
87
+ text_parts.append(part.get("text", ""))
88
+ elif isinstance(part, str):
89
+ text_parts.append(part)
90
+ content = "\n".join(text_parts) if text_parts else ""
91
+ elif not isinstance(content, str):
92
+ content = str(content)
93
+
94
+ # For assistant messages with tool_calls, embed tool call info in content text
95
+ if role == "assistant" and msg.get("tool_calls"):
96
+ tc_descriptions = []
97
+ for tc in msg["tool_calls"]:
98
+ if isinstance(tc, dict):
99
+ fn = tc.get("function", {})
100
+ name = fn.get("name", "unknown")
101
+ args = fn.get("arguments", "{}")
102
+ tc_descriptions.append(f"[Called tool: {name}({args})]")
103
+ if tc_descriptions:
104
+ content = (content + "\n" + "\n".join(tc_descriptions)).strip()
105
+ # Do NOT include tool_calls key — GGUF template will crash on it
106
+
107
+ clean["content"] = content
108
+ sanitized.append(clean)
109
+
110
+ return sanitized
111
+
112
+ def chat_completion(
113
+ self,
114
+ messages: List[dict],
115
+ tools: Optional[List[dict]] = None,
116
+ temperature: float = 0.2,
117
+ max_tokens: int = 2048,
118
+ enable_thinking: bool = True,
119
+ ) -> ChatResponse:
120
+ model = self._ensure_model()
121
+ sanitized_msgs = self._sanitize_messages_for_gguf(messages)
122
+ resp = model.create_chat_completion(
123
+ messages=sanitized_msgs,
124
+ tools=tools,
125
+ tool_choice="auto",
126
+ temperature=temperature,
127
+ max_tokens=max_tokens,
128
+ enable_thinking=enable_thinking,
129
+ )
130
+ msg = resp.choices[0].message
131
+
132
+ tool_calls = None
133
+ if msg.tool_calls:
134
+ import json
135
+ tool_calls = []
136
+ for tc in msg.tool_calls:
137
+ args = tc.function.arguments
138
+ if isinstance(args, str):
139
+ try:
140
+ args = json.loads(args or "{}")
141
+ except Exception:
142
+ args = {}
143
+ tool_calls.append(
144
+ ToolCall(
145
+ id=tc.id,
146
+ name=tc.function.name,
147
+ arguments=args if isinstance(args, dict) else {},
148
+ )
149
+ )
150
+
151
+ reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
152
+
153
+ return ChatResponse(
154
+ content=msg.content,
155
+ reasoning=reasoning,
156
+ tool_calls=tool_calls,
157
+ raw=resp,
158
+ )
159
+
160
+ def chat_completion_stream(
161
+ self,
162
+ messages: List[dict],
163
+ tools: Optional[List[dict]] = None,
164
+ temperature: Optional[float] = None,
165
+ max_tokens: Optional[int] = None,
166
+ enable_thinking: bool = True,
167
+ ):
168
+ """
169
+ Stream local GGUF completion yielding progressive thinking deltas,
170
+ content tokens, and the final ChatResponse.
171
+ """
172
+ import time
173
+ start_time = time.time()
174
+ temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.2)
175
+ m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 2048)
176
+
177
+ model = self._ensure_model()
178
+ sanitized_msgs = self._sanitize_messages_for_gguf(messages)
179
+
180
+ chunks = model.create_chat_completion(
181
+ messages=sanitized_msgs,
182
+ tools=tools,
183
+ tool_choice="auto" if tools else None,
184
+ temperature=temp,
185
+ max_tokens=m_tokens,
186
+ enable_thinking=enable_thinking,
187
+ stream=True,
188
+ )
189
+
190
+ try:
191
+ from .openai_backend import _repair_and_parse_json_arguments
192
+ except Exception:
193
+ import json
194
+ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
195
+ try:
196
+ return json.loads(raw_args or "{}")
197
+ except Exception:
198
+ return {}
199
+
200
+ in_think = enable_thinking
201
+ think_started = False
202
+ full_content = []
203
+ full_reasoning = []
204
+ tool_calls_dict = {}
205
+
206
+ for chunk in chunks:
207
+ if not chunk or not chunk.get("choices"):
208
+ continue
209
+ choice = chunk["choices"][0]
210
+ delta = choice.get("delta") or {}
211
+
212
+ explicit_reasoning = delta.get("reasoning") or delta.get("reasoning_content")
213
+ if explicit_reasoning:
214
+ if not think_started:
215
+ think_started = True
216
+ yield ("thinking_start", {"time": start_time})
217
+ full_reasoning.append(explicit_reasoning)
218
+ yield ("thinking_delta", {"delta": explicit_reasoning})
219
+
220
+ if delta.get("tool_calls"):
221
+ for tc in delta["tool_calls"]:
222
+ idx = tc.get("index", 0)
223
+ if idx not in tool_calls_dict:
224
+ fn = tc.get("function") or {}
225
+ tool_calls_dict[idx] = {
226
+ "id": tc.get("id") or f"call_{idx}",
227
+ "name": fn.get("name", ""),
228
+ "arguments": fn.get("arguments", ""),
229
+ }
230
+ else:
231
+ if tc.get("id"):
232
+ tool_calls_dict[idx]["id"] = tc["id"]
233
+ fn = tc.get("function") or {}
234
+ if fn.get("name"):
235
+ tool_calls_dict[idx]["name"] += fn["name"]
236
+ if fn.get("arguments"):
237
+ tool_calls_dict[idx]["arguments"] += fn["arguments"]
238
+
239
+ text = delta.get("content")
240
+ if text:
241
+ if in_think:
242
+ if not think_started:
243
+ think_started = True
244
+ yield ("thinking_start", {"time": start_time})
245
+
246
+ if "</think>" in text:
247
+ before, after = text.split("</think>", 1)
248
+ if before:
249
+ full_reasoning.append(before)
250
+ yield ("thinking_delta", {"delta": before})
251
+ in_think = False
252
+ elapsed = time.time() - start_time
253
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
254
+ if after.strip():
255
+ full_content.append(after)
256
+ yield ("token", {"delta": after})
257
+ elif "DONE:" in text:
258
+ before, after = text.split("DONE:", 1)
259
+ if before:
260
+ full_reasoning.append(before)
261
+ yield ("thinking_delta", {"delta": before})
262
+ in_think = False
263
+ elapsed = time.time() - start_time
264
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
265
+ done_chunk = "DONE:" + after
266
+ full_content.append(done_chunk)
267
+ yield ("token", {"delta": done_chunk})
268
+ else:
269
+ clean_text = text.replace("<think>", "")
270
+ full_reasoning.append(clean_text)
271
+ yield ("thinking_delta", {"delta": clean_text})
272
+ else:
273
+ full_content.append(text)
274
+ yield ("token", {"delta": text})
275
+
276
+ if in_think and think_started:
277
+ elapsed = time.time() - start_time
278
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
279
+
280
+ tool_calls = None
281
+ if tool_calls_dict:
282
+ tool_calls = []
283
+ for idx in sorted(tool_calls_dict.keys()):
284
+ item = tool_calls_dict[idx]
285
+ args = _repair_and_parse_json_arguments(item.get("arguments", ""))
286
+ tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
287
+
288
+ final_content_str = "".join(full_content).strip()
289
+ final_reasoning_str = "".join(full_reasoning).strip()
290
+
291
+ if not final_content_str and final_reasoning_str:
292
+ if "DONE:" in final_reasoning_str:
293
+ idx = final_reasoning_str.find("DONE:")
294
+ final_content_str = final_reasoning_str[idx:].strip()
295
+ final_reasoning_str = final_reasoning_str[:idx].strip()
296
+ yield ("token", {"delta": final_content_str})
297
+ elif "</think>" in final_reasoning_str:
298
+ parts = final_reasoning_str.split("</think>", 1)
299
+ final_reasoning_str = parts[0].strip()
300
+ final_content_str = parts[1].strip()
301
+ yield ("token", {"delta": final_content_str})
302
+
303
+ chat_resp = ChatResponse(
304
+ content=final_content_str,
305
+ reasoning=final_reasoning_str or None,
306
+ tool_calls=tool_calls,
307
+ raw=None,
308
+ )
309
+ yield ("done", chat_resp)
310
+
311
+ def shutdown(self) -> None:
312
+ if self._model:
313
+ self._model._llm = None
314
+ self._model = None
@@ -30,6 +30,8 @@ DEFAULTS = {
30
30
  "temperature": 0.1,
31
31
  "max_tokens": 8192,
32
32
  "enable_thinking": False, # Reasoning OFF by default
33
+ "adaptive_thinking": True, # Adaptive turn-level reasoning dynamics
34
+ "force_thinking": False, # Force reasoning on every single turn
33
35
  "n_ctx": 32768, # 32k context window
34
36
  "n_gpu_layers": None, # None = auto-detect GPU
35
37
  "config_version": CONFIG_VERSION,
@@ -92,6 +94,8 @@ class Config:
92
94
  temperature: float = 0.1
93
95
  max_tokens: int = 8192
94
96
  enable_thinking: bool = False
97
+ adaptive_thinking: bool = True
98
+ force_thinking: bool = False
95
99
  n_ctx: int = 32768
96
100
  n_gpu_layers: Optional[int] = None
97
101
 
@@ -220,6 +220,11 @@ def scan_repository(workdir: Path) -> dict:
220
220
  Designed to run in under 30ms by checking only key indicator files.
221
221
  """
222
222
  workdir = workdir.resolve()
223
+ if not workdir.exists():
224
+ try:
225
+ workdir.mkdir(parents=True, exist_ok=True)
226
+ except Exception:
227
+ pass
223
228
 
224
229
  has_git = (workdir / ".git").is_dir()
225
230
  has_venv = (workdir / ".venv").is_dir() or (workdir / "venv").is_dir()
@@ -407,7 +412,7 @@ def _has_test_files(workdir: Path) -> bool:
407
412
  for f in workdir.iterdir():
408
413
  if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py")):
409
414
  return True
410
- except PermissionError:
415
+ except (PermissionError, FileNotFoundError):
411
416
  pass
412
417
  return False
413
418
 
@@ -12,7 +12,7 @@ import shutil
12
12
  import socket
13
13
  import subprocess
14
14
  from pathlib import Path
15
- from typing import Dict, List, Tuple
15
+ from typing import Any, Dict, List, Tuple
16
16
  from urllib.parse import urlparse
17
17
 
18
18
  from .config import load_config, is_server_reachable, is_internet_available
@@ -45,24 +45,54 @@ def _check_command(cmd: str) -> Tuple[bool, str]:
45
45
  return True, "installed"
46
46
 
47
47
 
48
- def _check_gpu() -> Tuple[bool, str]:
49
- """Check NVIDIA GPU and CUDA support."""
50
- nvidia_smi = shutil.which("nvidia-smi")
51
- if not nvidia_smi:
52
- return False, "No NVIDIA GPU detected (CPU mode)"
48
+ def _check_gpu() -> Dict[str, Any]:
49
+ """Check both physical GPU hardware and llama-cpp runtime offload support."""
50
+ hardware_gpu = None
51
+ nvidia_smi = (
52
+ shutil.which("nvidia-smi")
53
+ or shutil.which("nvidia-smi.exe")
54
+ or (shutil.which("/usr/lib/wsl/lib/nvidia-smi") if Path("/usr/lib/wsl/lib/nvidia-smi").exists() else None)
55
+ )
56
+ if nvidia_smi:
57
+ try:
58
+ res = subprocess.run(
59
+ [str(nvidia_smi), "--query-gpu=name,memory.total", "--format=csv,noheader"],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=3,
63
+ )
64
+ if res.returncode == 0 and res.stdout.strip():
65
+ hardware_gpu = f"NVIDIA GPU: {res.stdout.strip().splitlines()[0]}"
66
+ else:
67
+ hardware_gpu = "NVIDIA GPU driver present"
68
+ except Exception:
69
+ hardware_gpu = "NVIDIA GPU driver present"
70
+ elif sys.platform == "darwin":
71
+ import platform
72
+ if "arm" in platform.processor().lower():
73
+ hardware_gpu = "Apple Silicon (Metal)"
74
+
75
+ runtime_offload = False
76
+ runtime_backend = "CPU only"
53
77
  try:
54
- res = subprocess.run(
55
- [nvidia_smi, "--query-gpu=name,memory.total", "--format=csv,noheader"],
56
- capture_output=True,
57
- text=True,
58
- timeout=3,
59
- )
60
- if res.returncode == 0 and res.stdout.strip():
61
- gpu_info = res.stdout.strip().split("\n")[0]
62
- return True, f"NVIDIA GPU: {gpu_info}"
78
+ import llama_cpp
79
+ runtime_offload = bool(getattr(llama_cpp, "llama_supports_gpu_offload", lambda: False)())
80
+ if runtime_offload:
81
+ if hardware_gpu and "NVIDIA" in hardware_gpu:
82
+ runtime_backend = "CUDA"
83
+ elif hardware_gpu and "Apple" in hardware_gpu:
84
+ runtime_backend = "Metal"
85
+ else:
86
+ runtime_backend = "GPU Offload"
63
87
  except Exception:
64
88
  pass
65
- return True, "NVIDIA GPU driver present"
89
+
90
+ return {
91
+ "has_hardware": hardware_gpu is not None,
92
+ "hardware_info": hardware_gpu or "No dedicated GPU detected (CPU mode)",
93
+ "runtime_offload": runtime_offload,
94
+ "runtime_backend": runtime_backend,
95
+ }
66
96
 
67
97
 
68
98
  def _check_remote_latency(base_url: str) -> Tuple[bool, str]:
@@ -131,13 +161,23 @@ def run_doctor() -> int:
131
161
  # 2. CUDA / Hardware Acceleration
132
162
  print()
133
163
  print(_colorize(" 🖥️ Hardware & GPU Acceleration", "cyan"))
134
- has_gpu, gpu_msg = _check_gpu()
135
- if has_gpu:
136
- print(f" {_colorize('✅', 'green')} {gpu_msg}")
164
+ gpu_diag = _check_gpu()
165
+ if gpu_diag["has_hardware"]:
166
+ print(f" {_colorize('✅', 'green')} Hardware: {gpu_diag['hardware_info']}")
137
167
  passed_checks += 1
168
+ if gpu_diag["runtime_offload"]:
169
+ print(f" {_colorize('✅', 'green')} Python Runtime: llama-cpp-python ({gpu_diag['runtime_backend']} acceleration enabled)")
170
+ passed_checks += 1
171
+ else:
172
+ print(f" {_colorize('⚠️ ', 'yellow')} Python Runtime: llama-cpp-python is running in CPU mode (no CUDA build)")
173
+ print(f" {_colorize('💡', 'cyan')} To enable 5-10x faster GPU acceleration, install the CUDA wheel:")
174
+ print(f" {_colorize('pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124', 'bold')}")
138
175
  else:
139
- print(f" {_colorize('ℹ️ ', 'blue')} {gpu_msg}")
140
- total_checks += 1
176
+ print(f" {_colorize('ℹ️ ', 'blue')} {gpu_diag['hardware_info']}")
177
+ if gpu_diag["runtime_offload"]:
178
+ print(f" {_colorize('ℹ️ ', 'dim')} Python Runtime: llama-cpp-python GPU offload ready")
179
+ passed_checks += 1
180
+ total_checks += 2
141
181
 
142
182
  # 3. Network & Connectivity
143
183
  print()
@@ -166,26 +206,26 @@ def run_doctor() -> int:
166
206
  print()
167
207
  print(_colorize(" 🧰 Development Tools & Compilers", "cyan"))
168
208
  tools_to_check = [
169
- ("node", "Node.js (JavaScript/TypeScript runtime)"),
170
- ("npm", "Node Package Manager"),
171
- ("gcc", "GNU C Compiler"),
172
- ("g++", "GNU C++ Compiler"),
173
- ("git", "Git Version Control"),
174
- ("go", "Go Programming Language (optional)"),
175
- ("rustc", "Rust Compiler (optional)"),
176
- ("java", "Java JDK (optional)"),
209
+ ("git", "Git Version Control", False),
210
+ ("gcc", "GNU C Compiler", False),
211
+ ("g++", "GNU C++ Compiler", False),
212
+ ("node", "Node.js (JavaScript/TypeScript runtime, optional)", True),
213
+ ("npm", "Node Package Manager (optional)", True),
214
+ ("go", "Go Programming Language (optional)", True),
215
+ ("rustc", "Rust Compiler (optional)", True),
216
+ ("java", "Java JDK (optional)", True),
177
217
  ]
178
- for tool_name, desc in tools_to_check:
218
+ for tool_name, desc, is_optional in tools_to_check:
179
219
  ok, msg = _check_command(tool_name)
180
- is_optional = "optional" in desc
181
220
  if ok:
182
221
  print(f" {_colorize('✅', 'green')} {tool_name:8s} : {msg}")
183
222
  passed_checks += 1
223
+ total_checks += 1
184
224
  elif is_optional:
185
225
  print(f" {_colorize('ℹ️ ', 'dim')} {tool_name:8s} : not installed (optional)")
186
226
  else:
187
227
  print(f" {_colorize('⚠️ ', 'yellow')} {tool_name:8s} : not installed")
188
- total_checks += 1
228
+ total_checks += 1
189
229
 
190
230
  # 5. VS Code Integration
191
231
  print()
@@ -313,6 +313,7 @@ class LocalModel:
313
313
 
314
314
  if self.n_gpu_layers != 0 and not has_gpu_offload:
315
315
  print(" ⚠️ Warning: Active Python environment has no CUDA llama-cpp support. Running on CPU.")
316
+ print(" 💡 Tip: Run 'alpiecode doctor' for the one-line command to install the CUDA GPU wheel.")
316
317
  accel = "CPU (No CUDA in Python env)"
317
318
  self.n_gpu_layers = 0
318
319
  else:
@@ -362,7 +363,9 @@ class LocalModel:
362
363
  model_path=str(model_path),
363
364
  n_ctx=self.n_ctx,
364
365
  n_batch=2048 if self.n_gpu_layers == 0 else 4096,
366
+ n_ubatch=512,
365
367
  n_threads=n_threads,
368
+ n_threads_batch=max(1, os.cpu_count() or 4),
366
369
  n_gpu_layers=self.n_gpu_layers,
367
370
  use_mmap=True,
368
371
  flash_attn=(self.n_gpu_layers != 0), # Flash Attention only for GPU
@@ -381,6 +384,14 @@ class LocalModel:
381
384
 
382
385
  try:
383
386
  self._llm = Llama(**llama_kwargs)
387
+ # In-Memory KV Prompt Prefix Caching for instant Turn 2+ speedup
388
+ try:
389
+ import llama_cpp
390
+ if hasattr(llama_cpp, "LlamaRAMCache") and hasattr(self._llm, "set_cache"):
391
+ cache = llama_cpp.LlamaRAMCache(capacity_bytes=512 * 1024 * 1024)
392
+ self._llm.set_cache(cache)
393
+ except Exception:
394
+ pass
384
395
  finally:
385
396
  # Restore stderr
386
397
  if _stderr_fd is not None:
@@ -425,15 +436,20 @@ class LocalModel:
425
436
  except Exception:
426
437
  pass
427
438
 
439
+ stream = kwargs.get("stream", False)
428
440
  params = {
429
441
  "messages": msgs,
430
442
  "temperature": 0.1 if not enable_thinking else temperature,
431
443
  "max_tokens": max_tokens,
444
+ "stream": stream,
432
445
  }
433
446
  if tools:
434
447
  params["tools"] = tools
435
448
  params["tool_choice"] = kwargs.get("tool_choice", "auto")
436
449
 
450
+ if stream:
451
+ return llm.create_chat_completion(**params)
452
+
437
453
  try:
438
454
  result_dict = llm.create_chat_completion(**params)
439
455
  except ValueError as ve: