alpiecode 10.6.7__tar.gz → 10.6.9__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 (59) hide show
  1. {alpiecode-10.6.7 → alpiecode-10.6.9}/PKG-INFO +1 -1
  2. {alpiecode-10.6.7 → alpiecode-10.6.9}/pyproject.toml +1 -1
  3. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/__init__.py +1 -1
  5. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/agent.py +6 -1
  6. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/backends/local_backend.py +271 -1
  7. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/backends/openai_backend.py +13 -1
  8. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/config.py +20 -1
  9. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/local_model.py +4 -1
  10. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/orchestrator.py +7 -2
  11. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/prompt.py +14 -1
  12. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/rephraser.py +7 -1
  13. alpiecode-10.6.9/test/test_non_thinking_and_local_stream.py +136 -0
  14. alpiecode-10.6.7/test/test_non_thinking_and_local_stream.py +0 -70
  15. {alpiecode-10.6.7 → alpiecode-10.6.9}/README.md +0 -0
  16. {alpiecode-10.6.7 → alpiecode-10.6.9}/setup.cfg +0 -0
  17. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode/__init__.py +0 -0
  18. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  19. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  20. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/entry_points.txt +0 -0
  21. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/requires.txt +0 -0
  22. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/alpiecode.egg-info/top_level.txt +0 -0
  23. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/assessment.py +0 -0
  24. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/backends/__init__.py +0 -0
  25. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/backends/base.py +0 -0
  26. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/cache.py +0 -0
  27. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/cli.py +0 -0
  28. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/client.py +0 -0
  29. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/compaction.py +0 -0
  30. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/context.py +0 -0
  31. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/discovery.py +0 -0
  32. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/doctor.py +0 -0
  33. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/dream_rsi.py +0 -0
  34. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/executor.py +0 -0
  35. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/extension/alpiecode.vsix +0 -0
  36. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/git_ops.py +0 -0
  37. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/github.py +0 -0
  38. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/guardian.py +0 -0
  39. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/guardrails.py +0 -0
  40. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/ipython_ext.py +0 -0
  41. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/media.py +0 -0
  42. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/memory.py +0 -0
  43. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/metrics.py +0 -0
  44. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/paths.py +0 -0
  45. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/progress.py +0 -0
  46. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/server.py +0 -0
  47. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/session.py +0 -0
  48. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/system1.py +0 -0
  49. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/tools.py +0 -0
  50. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/updater.py +0 -0
  51. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/verifier.py +0 -0
  52. {alpiecode-10.6.7 → alpiecode-10.6.9}/src/codeagent/vscode_installer.py +0 -0
  53. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_adaptive_resilience.py +0 -0
  54. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_cli_ui.py +0 -0
  55. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_context_and_trajectory.py +0 -0
  56. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_dream_rsi.py +0 -0
  57. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_local_model_download.py +0 -0
  58. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_task_assessment.py +0 -0
  59. {alpiecode-10.6.7 → alpiecode-10.6.9}/test/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.6.7
3
+ Version: 10.6.9
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.6.7"
7
+ version = "10.6.9"
8
8
  description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.6.7
3
+ Version: 10.6.9
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -17,7 +17,7 @@ Supports:
17
17
  from pathlib import Path
18
18
  from typing import Optional, Any, Dict
19
19
 
20
- __version__ = "10.6.7"
20
+ __version__ = "10.6.9"
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
23
23
  """Run an autonomous coding task programmatically."""
@@ -734,7 +734,12 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True, debug: bool = Fal
734
734
  session_mgr = SessionManager()
735
735
  session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
736
736
 
737
- is_offline = isinstance(backend, LocalBackend) or not backend.is_available
737
+ is_offline = (
738
+ getattr(backend, "is_offline", False) is True
739
+ or isinstance(backend, LocalBackend)
740
+ or not backend.is_available
741
+ or any(h in str(getattr(cfg, "base_url", "")).lower() for h in ("127.0.0.1", "localhost", "0.0.0.0", "offline"))
742
+ )
738
743
  active_tools = PromptBuilder().get_tools(is_offline=is_offline)
739
744
 
740
745
  if HAS_RICH:
@@ -39,6 +39,10 @@ class LocalBackend:
39
39
  def is_available(self) -> bool:
40
40
  return True
41
41
 
42
+ @property
43
+ def is_offline(self) -> bool:
44
+ return True
45
+
42
46
 
43
47
  @property
44
48
  def context_window(self) -> int:
@@ -401,6 +405,166 @@ class LocalBackend:
401
405
  )
402
406
  yield ("done", chat_resp)
403
407
 
408
+ def _get_bridge_url(self) -> str:
409
+ return os.environ.get("LOCAL_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
410
+
411
+ def _is_bridge_online(self) -> bool:
412
+ try:
413
+ import httpx
414
+ r = httpx.get(f"{self._get_bridge_url()}/v1/models", timeout=0.3)
415
+ return r.status_code == 200
416
+ except Exception:
417
+ return False
418
+
419
+ def _bridge_chat_completion(
420
+ self,
421
+ messages: List[dict],
422
+ tools: Optional[List[dict]] = None,
423
+ temperature: float = 0.2,
424
+ max_tokens: int = 2048,
425
+ enable_thinking: bool = True,
426
+ ) -> ChatResponse:
427
+ import httpx
428
+ import json
429
+ bridge_url = self._get_bridge_url()
430
+ sanitized = self._sanitize_messages_for_gguf(messages)
431
+ if not enable_thinking and sanitized and sanitized[-1].get("role") == "user":
432
+ u_text = str(sanitized[-1].get("content", ""))
433
+ if "[Direct Mode:" not in u_text:
434
+ sanitized[-1]["content"] = u_text + "\n\n[Direct Mode: Output code and tool calls directly without <think> tags. Do not deliberate.]"
435
+ payload = {
436
+ "model": "local",
437
+ "messages": sanitized,
438
+ "temperature": temperature,
439
+ "max_tokens": max_tokens,
440
+ "stream": False,
441
+ }
442
+ if tools:
443
+ payload["tools"] = tools
444
+ payload["tool_choice"] = "auto"
445
+ client = httpx.Client(http2=False, timeout=120.0)
446
+ try:
447
+ resp = client.post(f"{bridge_url}/v1/chat/completions", json=payload)
448
+ finally:
449
+ client.close()
450
+ if resp.status_code != 200:
451
+ raise RuntimeError(f"Local bridge HTTP {resp.status_code}: {resp.text}")
452
+ data = json.loads(resp.text)
453
+ choice = data["choices"][0]["message"]
454
+ content = choice.get("content") or ""
455
+ reasoning = choice.get("reasoning") or choice.get("reasoning_content")
456
+ if not enable_thinking:
457
+ reasoning = None
458
+ if "<think>" in content:
459
+ import re
460
+ content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
461
+ raw_tc = choice.get("tool_calls")
462
+ tool_calls = None
463
+ if raw_tc:
464
+ tool_calls = []
465
+ for tc in raw_tc:
466
+ fn = tc.get("function", {})
467
+ args = fn.get("arguments", "{}")
468
+ if isinstance(args, str):
469
+ try:
470
+ args = json.loads(args)
471
+ except Exception:
472
+ args = {}
473
+ tool_calls.append(ToolCall(id=tc.get("id", "call_1"), name=fn.get("name", ""), arguments=args))
474
+ return ChatResponse(content=content, reasoning=reasoning, tool_calls=tool_calls, raw=data)
475
+
476
+ def _bridge_chat_completion_stream(
477
+ self,
478
+ messages: List[dict],
479
+ tools: Optional[List[dict]] = None,
480
+ temperature: Optional[float] = None,
481
+ max_tokens: Optional[int] = None,
482
+ enable_thinking: bool = True,
483
+ ):
484
+ import httpx
485
+ import json
486
+ bridge_url = self._get_bridge_url()
487
+ sanitized = self._sanitize_messages_for_gguf(messages)
488
+ if not enable_thinking and sanitized and sanitized[-1].get("role") == "user":
489
+ u_text = str(sanitized[-1].get("content", ""))
490
+ if "[Direct Mode:" not in u_text:
491
+ sanitized[-1]["content"] = u_text + "\n\n[Direct Mode: Output code and tool calls directly without <think> tags. Do not deliberate.]"
492
+ payload = {
493
+ "model": "local",
494
+ "messages": sanitized,
495
+ "temperature": temperature or 0.2,
496
+ "max_tokens": max_tokens or 2048,
497
+ "stream": True,
498
+ }
499
+ if tools:
500
+ payload["tools"] = tools
501
+ payload["tool_choice"] = "auto"
502
+ client = httpx.Client(http2=False, timeout=120.0)
503
+ req = client.build_request("POST", f"{bridge_url}/v1/chat/completions", json=payload)
504
+ resp = client.send(req, stream=True)
505
+ if resp.status_code != 200:
506
+ err = resp.read().decode("utf-8", errors="replace")
507
+ resp.close()
508
+ client.close()
509
+ raise RuntimeError(f"Local bridge HTTP {resp.status_code}: {err}")
510
+ full_content = []
511
+ full_reasoning = []
512
+ tool_calls = []
513
+ suppress_think = not enable_thinking
514
+ inside_suppressed_think = False
515
+ try:
516
+ for line in resp.iter_lines():
517
+ line = line.strip()
518
+ if not line or not line.startswith("data: "):
519
+ continue
520
+ data_str = line[6:].strip()
521
+ if data_str == "[DONE]":
522
+ break
523
+ try:
524
+ chunk = json.loads(data_str)
525
+ except Exception:
526
+ continue
527
+ choice = chunk["choices"][0]
528
+ delta = choice.get("delta") or {}
529
+ text = delta.get("content")
530
+ if text:
531
+ if suppress_think:
532
+ if "<think>" in text:
533
+ inside_suppressed_think = True
534
+ text = text.replace("<think>", "")
535
+ if inside_suppressed_think:
536
+ if "<tool_call>" in text or "DONE:" in text or "<function=" in text:
537
+ inside_suppressed_think = False
538
+ elif "</think>" in text:
539
+ _, after = text.split("</think>", 1)
540
+ inside_suppressed_think = False
541
+ text = after
542
+ else:
543
+ text = ""
544
+ else:
545
+ text = text.replace("</think>", "")
546
+ if not text:
547
+ continue
548
+ full_content.append(text)
549
+ yield ("token", {"delta": text})
550
+ continue
551
+
552
+ full_content.append(text)
553
+ yield ("token", {"delta": text})
554
+
555
+ if (delta.get("reasoning") or delta.get("reasoning_content")) and enable_thinking:
556
+ r = delta.get("reasoning") or delta.get("reasoning_content")
557
+ full_reasoning.append(r)
558
+ yield ("thinking_delta", {"delta": r, "count": len(full_reasoning)})
559
+ finally:
560
+ resp.close()
561
+ client.close()
562
+ yield ("done", ChatResponse(
563
+ content="".join(full_content).strip(),
564
+ reasoning="".join(full_reasoning).strip() or None,
565
+ tool_calls=tool_calls or None,
566
+ ))
567
+
404
568
  def shutdown(self) -> None:
405
569
  if self._model:
406
570
  self._model._llm = None
@@ -840,13 +1004,119 @@ class AndroidBackend(BasePlatformBackend):
840
1004
  def platform_name(self) -> str:
841
1005
  return "Android"
842
1006
 
1007
+ def _get_bridge_url(self) -> str:
1008
+ return os.environ.get("ANDROID_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
1009
+
1010
+ def _ensure_local_server(self) -> bool:
1011
+ """Auto-detect or auto-start local llama-server in background if needed."""
1012
+ if self._is_bridge_online():
1013
+ return True
1014
+ server_bin = shutil.which("llama-server")
1015
+ if not server_bin:
1016
+ return False
1017
+ from ..local_model import get_model_cache_dir
1018
+ cache_dir = get_model_cache_dir()
1019
+ if not cache_dir.exists():
1020
+ return False
1021
+ ggufs = list(cache_dir.glob("*.gguf"))
1022
+ main_models = [g for g in ggufs if "mmproj" not in g.name.lower()]
1023
+ if not main_models:
1024
+ return False
1025
+ model_path = main_models[0]
1026
+ mmprojs = [g for g in ggufs if "mmproj" in g.name.lower()]
1027
+ cmd = [
1028
+ server_bin,
1029
+ "-m", str(model_path),
1030
+ "-c", "4096",
1031
+ "-t", "4",
1032
+ "-np", "1",
1033
+ "-b", "512",
1034
+ "-ub", "256",
1035
+ "--cache-type-k", "q8_0",
1036
+ "--cache-type-v", "q8_0",
1037
+ "--port", "8080",
1038
+ ]
1039
+ if mmprojs:
1040
+ cmd += ["--mmproj", str(mmprojs[0])]
1041
+ try:
1042
+ subprocess.Popen(
1043
+ cmd,
1044
+ stdout=subprocess.DEVNULL,
1045
+ stderr=subprocess.DEVNULL,
1046
+ start_new_session=True,
1047
+ )
1048
+ import time
1049
+ for _ in range(25):
1050
+ if self._is_bridge_online():
1051
+ return True
1052
+ time.sleep(0.4)
1053
+ except Exception:
1054
+ pass
1055
+ return self._is_bridge_online()
1056
+
843
1057
  @property
844
1058
  def is_available(self) -> bool:
845
1059
  try:
846
1060
  import llama_cpp
847
1061
  return True
848
1062
  except ImportError:
849
- return bool(shutil.which("llama-server") or shutil.which("llama-cli"))
1063
+ return self._is_bridge_online() or bool(shutil.which("llama-server") or shutil.which("llama-cli"))
1064
+
1065
+ def chat_completion(
1066
+ self,
1067
+ messages: List[dict],
1068
+ tools: Optional[List[dict]] = None,
1069
+ temperature: float = 0.2,
1070
+ max_tokens: int = 2048,
1071
+ enable_thinking: bool = True,
1072
+ ) -> ChatResponse:
1073
+ try:
1074
+ import llama_cpp
1075
+ return super().chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
1076
+ except ImportError:
1077
+ pass
1078
+
1079
+ if self._ensure_local_server():
1080
+ return self._bridge_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
1081
+
1082
+ raise RuntimeError(
1083
+ "\n╭────────────────────────────────────────────────────────────╮\n"
1084
+ "│ Android / Termux Native Inference: │\n"
1085
+ "│ No local engine is available. │\n"
1086
+ "│ To enable local inference on Termux, run: │\n"
1087
+ "│ pkg install llama-cpp │\n"
1088
+ "│ alpiecode init │\n"
1089
+ "╰────────────────────────────────────────────────────────────╯"
1090
+ )
1091
+
1092
+ def chat_completion_stream(
1093
+ self,
1094
+ messages: List[dict],
1095
+ tools: Optional[List[dict]] = None,
1096
+ temperature: Optional[float] = None,
1097
+ max_tokens: Optional[int] = None,
1098
+ enable_thinking: bool = True,
1099
+ ):
1100
+ try:
1101
+ import llama_cpp
1102
+ yield from super().chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
1103
+ return
1104
+ except ImportError:
1105
+ pass
1106
+
1107
+ if self._ensure_local_server():
1108
+ yield from self._bridge_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
1109
+ return
1110
+
1111
+ raise RuntimeError(
1112
+ "\n╭────────────────────────────────────────────────────────────╮\n"
1113
+ "│ Android / Termux Native Inference: │\n"
1114
+ "│ No local engine is available. │\n"
1115
+ "│ To enable local inference on Termux, run: │\n"
1116
+ "│ pkg install llama-cpp │\n"
1117
+ "│ alpiecode init │\n"
1118
+ "╰────────────────────────────────────────────────────────────╯"
1119
+ )
850
1120
 
851
1121
 
852
1122
  class MacOSBackend(BasePlatformBackend):
@@ -293,7 +293,7 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
293
293
  "_parse_error": "Failed to parse streaming JSON arguments",
294
294
  }
295
295
 
296
- from ..config import Config, get_shared_http_client, is_server_reachable
296
+ from ..config import Config, get_shared_http_client, is_server_reachable, is_local_endpoint
297
297
  from .base import ChatResponse, ToolCall
298
298
 
299
299
 
@@ -306,6 +306,18 @@ class OpenAIBackend:
306
306
  self._resolved_model: Optional[str] = None
307
307
  self._max_model_len: Optional[int] = None
308
308
 
309
+ @property
310
+ def base_url(self) -> str:
311
+ return getattr(self._cfg, "base_url", "")
312
+
313
+ @property
314
+ def is_local(self) -> bool:
315
+ return is_local_endpoint(self.base_url)
316
+
317
+ @property
318
+ def is_offline(self) -> bool:
319
+ return self.is_local
320
+
309
321
  @property
310
322
  def name(self) -> str:
311
323
  return f"Online API ({self._cfg.base_url})"
@@ -14,7 +14,7 @@ from dataclasses import dataclass, asdict, fields
14
14
  from pathlib import Path
15
15
  from typing import Optional
16
16
 
17
- from .paths import ensure_dir, get_alpiecode_dir, is_ios, load_dotenv_if_present
17
+ from .paths import ensure_dir, get_alpiecode_dir, is_android, is_ios, load_dotenv_if_present
18
18
 
19
19
  # Auto-load .env if present (zero-dependency)
20
20
  load_dotenv_if_present()
@@ -120,6 +120,14 @@ def is_internet_available(timeout: float = 1.0) -> bool:
120
120
  return False
121
121
 
122
122
 
123
+ def is_local_endpoint(base_url: Optional[str]) -> bool:
124
+ """Check if base_url points to a local machine / loopback interface or offline mode."""
125
+ if not base_url:
126
+ return False
127
+ u = str(base_url).lower().strip()
128
+ return any(h in u for h in ("127.0.0.1", "localhost", "0.0.0.0", "::1", "offline"))
129
+
130
+
123
131
  # ── Shared HTTP/2 connection pool ─────────────────────────────────────
124
132
  _SHARED_HTTP_CLIENT = None
125
133
 
@@ -299,6 +307,17 @@ def interactive_init() -> Config:
299
307
  else:
300
308
  print("ℹ️ iOS Inference: Online model endpoint (https://test.169pi.ai) is configured.")
301
309
  print(" For offline inference on iOS, launch an in-app bridge (http://127.0.0.1:8080).")
310
+ elif is_android():
311
+ print("\n⚙️ Configuring inference engine for Android...")
312
+ from .backends.local_backend import AndroidBackend
313
+ b = AndroidBackend(cfg)
314
+ if b.is_available:
315
+ print("✅ Android local engine ready (native llama-cpp or local server active).")
316
+ print("✅ Offline mode ready — works without internet from now on!")
317
+ else:
318
+ print("ℹ️ To enable local offline inference on Termux, please install:")
319
+ print(" pkg install llama-cpp")
320
+ print(" Then run: alpiecode init")
302
321
  else:
303
322
  try:
304
323
  from .local_model import _ensure_llama_cpp
@@ -258,12 +258,15 @@ def _ensure_llama_cpp():
258
258
  )
259
259
 
260
260
  if is_android():
261
+ import shutil
262
+ if shutil.which("llama-server") or shutil.which("llama-cli"):
263
+ return None
261
264
  raise RuntimeError(
262
265
  "\n╭────────────────────────────────────────────────────────────╮\n"
263
266
  "│ Android / Termux Native Inference: │\n"
264
267
  "│ Dynamic pip compilation is disabled on Android. │\n"
265
268
  "│ To enable local inference on Termux, please install: │\n"
266
- "│ pkg install llama.cpp │\n"
269
+ "│ pkg install llama-cpp │\n"
267
270
  "╰────────────────────────────────────────────────────────────╯"
268
271
  )
269
272
 
@@ -47,7 +47,7 @@ from .backends.base import InferenceBackend, ChatResponse
47
47
  from .backends.local_backend import LocalBackend
48
48
  from .backends.openai_backend import OpenAIBackend
49
49
  from .cache import get_cache
50
- from .config import Config, is_server_reachable, is_internet_available
50
+ from .config import Config, is_server_reachable, is_internet_available, is_local_endpoint
51
51
  from .memory import extract_and_save_memories
52
52
  from .discovery import build_task_context, gather_relevant_context, COMPLEXITY_CONFIG
53
53
  from .progress import ProgressMonitor
@@ -251,7 +251,12 @@ class AgentOrchestrator:
251
251
  if isinstance(task_backend, LocalBackend) and is_server_reachable(cfg.base_url, timeout=1.5):
252
252
  task_backend = OpenAIBackend(cfg)
253
253
 
254
- is_offline = not task_backend.is_available or isinstance(task_backend, LocalBackend)
254
+ is_offline = (
255
+ not task_backend.is_available
256
+ or getattr(task_backend, "is_offline", False) is True
257
+ or isinstance(task_backend, LocalBackend)
258
+ or is_local_endpoint(getattr(cfg, "base_url", ""))
259
+ )
255
260
  session.is_offline = is_offline
256
261
  # ?? Dream-RSI Trajectory Recorder ??
257
262
  trajectory_store = TrajectoryStore()
@@ -530,7 +530,20 @@ class PromptBuilder:
530
530
  """Build system prompt, optionally enhanced with TaskContext discovery."""
531
531
  if is_offline:
532
532
  prompt = OFFLINE_SYSTEM_PROMPT
533
- elif complexity == "high":
533
+ if task_context is not None:
534
+ shell = getattr(task_context, "shell", "bash")
535
+ if shell == "powershell":
536
+ prompt += "\n\n" + POWERSHELL_RULES
537
+ elif shell == "wsl":
538
+ prompt += "\n\n" + WSL_RULES
539
+ elif shell == "bash":
540
+ prompt += "\n\n" + BASH_RULES
541
+ memories = format_memories_for_prompt(workdir, max_entries=2, max_chars=250)
542
+ if memories:
543
+ prompt += f"\n\n{memories}"
544
+ return prompt
545
+
546
+ if complexity == "high":
534
547
  prompt = SYSTEM_PROMPT_HIGH
535
548
  else:
536
549
  prompt = SYSTEM_PROMPT
@@ -119,7 +119,13 @@ class PromptRephraser:
119
119
  return task
120
120
 
121
121
  # In offline/local GGUF mode, skip redundant rephraser LLM pass to ensure instant turn-0 start
122
- if type(backend).__name__ == "LocalBackend" or getattr(backend, "is_offline", None) is True:
122
+ is_local = (
123
+ type(backend).__name__ == "LocalBackend"
124
+ or getattr(backend, "is_offline", None) is True
125
+ or getattr(backend, "is_local", None) is True
126
+ or any(h in str(getattr(backend, "base_url", "")).lower() for h in ("127.0.0.1", "localhost", "0.0.0.0", "offline"))
127
+ )
128
+ if is_local:
123
129
  return task
124
130
 
125
131
  try:
@@ -0,0 +1,136 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock, patch
3
+ from codeagent.config import Config
4
+ from codeagent.backends.openai_backend import _HTTPXClient, OpenAIBackend
5
+
6
+
7
+ def test_httpx_client_uses_http1_for_local_bridge():
8
+ """Verify that localhost/127.0.0.1 uses an unbuffered HTTP/1.1 client for llama-server."""
9
+ client_local = _HTTPXClient("http://127.0.0.1:8080/v1", "not-needed")
10
+ # http2 is disabled for local server
11
+ assert client_local.http_client._transport._pool._http2 is False or not getattr(client_local.http_client, "_http2", True)
12
+
13
+ client_remote = _HTTPXClient("https://test.169pi.ai/v1", "not-needed")
14
+ assert client_remote.http_client is not None
15
+
16
+
17
+ def test_force_no_thinking_flag_on_config():
18
+ """Verify that force_no_thinking flag defaults to False and can be set to True."""
19
+ cfg = Config()
20
+ assert cfg.force_no_thinking is False
21
+
22
+ cfg_no_think = Config(force_no_thinking=True)
23
+ assert cfg_no_think.force_no_thinking is True
24
+
25
+
26
+ def test_openai_backend_suppresses_think_in_non_thinking_mode():
27
+ """Verify that OpenAIBackend suppresses think tokens when enable_thinking=False."""
28
+ cfg = Config(base_url="http://127.0.0.1:8080/v1")
29
+ backend = OpenAIBackend(cfg)
30
+
31
+ # Mock response chunks containing a <think> block
32
+ class MockDelta:
33
+ def __init__(self, content=None, tool_calls=None):
34
+ self.content = content
35
+ self.tool_calls = tool_calls
36
+
37
+ class MockChoice:
38
+ def __init__(self, delta):
39
+ self.delta = delta
40
+
41
+ class MockChunk:
42
+ def __init__(self, content):
43
+ self.choices = [MockChoice(MockDelta(content=content))]
44
+
45
+ mock_stream = [
46
+ MockChunk("<think>"),
47
+ MockChunk("Reasoning about prime numbers..."),
48
+ MockChunk("</think>"),
49
+ MockChunk("def is_prime(n):"),
50
+ MockChunk(" return n > 1"),
51
+ ]
52
+
53
+ mock_client = MagicMock()
54
+ mock_client.chat.completions.create.return_value = mock_stream
55
+ backend._client = mock_client
56
+ backend._resolved_model = "alpie_9b"
57
+
58
+ events = list(backend.chat_completion_stream(
59
+ messages=[{"role": "user", "content": "Write a prime checker"}],
60
+ enable_thinking=False,
61
+ ))
62
+
63
+ # In non-thinking mode, thinking_start and thinking_delta must NOT be emitted
64
+ event_types = [e[0] for e in events]
65
+ assert "thinking_start" not in event_types
66
+ assert "thinking_delta" not in event_types
67
+
68
+ # Only real content tokens must be emitted
69
+ token_events = [e[1]["delta"] for e in events if e[0] == "token"]
70
+ assert "".join(token_events).strip() == "def is_prime(n): return n > 1"
71
+
72
+
73
+ def test_android_backend_offline_bridge_routing():
74
+ """Verify that AndroidBackend routes to bridge when available."""
75
+ from codeagent.backends.local_backend import AndroidBackend
76
+ cfg = Config(base_url="offline")
77
+ backend = AndroidBackend(cfg)
78
+
79
+ # When bridge is online and llama_cpp is absent (Android), is_available is True
80
+ with patch.dict("sys.modules", {"llama_cpp": None}):
81
+ with patch.object(backend, "_is_bridge_online", return_value=True):
82
+ assert backend.is_available is True
83
+ with patch.object(backend, "_bridge_chat_completion_stream", return_value=iter([("token", {"delta": "hello"})])):
84
+ res = list(backend.chat_completion_stream([{"role": "user", "content": "hi"}]))
85
+ assert len(res) == 1
86
+ assert res[0] == ("token", {"delta": "hello"})
87
+
88
+
89
+ def test_is_local_endpoint():
90
+ from codeagent.config import is_local_endpoint
91
+ assert is_local_endpoint("http://127.0.0.1:8080/v1") is True
92
+ assert is_local_endpoint("http://localhost:8080/v1") is True
93
+ assert is_local_endpoint("http://0.0.0.0:8000") is True
94
+ assert is_local_endpoint("offline") is True
95
+ assert is_local_endpoint("https://test.169pi.ai/v1") is False
96
+ assert is_local_endpoint("https://api.openai.com/v1") is False
97
+
98
+
99
+ def test_openai_backend_is_offline_property():
100
+ cfg_local = Config(base_url="http://127.0.0.1:8080/v1")
101
+ b_local = OpenAIBackend(cfg_local)
102
+ assert b_local.is_local is True
103
+ assert b_local.is_offline is True
104
+
105
+ cfg_remote = Config(base_url="https://test.169pi.ai/v1")
106
+ b_remote = OpenAIBackend(cfg_remote)
107
+ assert b_remote.is_local is False
108
+ assert b_remote.is_offline is False
109
+
110
+
111
+ def test_rephraser_bypasses_for_local_backend():
112
+ from codeagent.rephraser import PromptRephraser
113
+ rephraser = PromptRephraser()
114
+ cfg = Config(base_url="http://127.0.0.1:8080/v1")
115
+ backend = OpenAIBackend(cfg)
116
+
117
+ # Calling rephrase should return original task immediately without calling backend.chat_completion
118
+ backend.chat_completion = MagicMock(side_effect=Exception("Should not be called!"))
119
+ raw_task = "Write a bash script to monitor memory usage"
120
+ res = rephraser.rephrase(raw_task, backend)
121
+ assert res == raw_task
122
+ backend.chat_completion.assert_not_called()
123
+
124
+
125
+ def test_prompt_builder_lean_offline_prompt():
126
+ from pathlib import Path
127
+ from codeagent.prompt import PromptBuilder, OFFLINE_SYSTEM_PROMPT
128
+ from codeagent.discovery import build_task_context
129
+
130
+ pb = PromptBuilder()
131
+ ctx = build_task_context("Build a full stack web app with auth", Path("."))
132
+ # When offline, prompt must be OFFLINE_SYSTEM_PROMPT (lean) and NOT include GOAL_DRIVEN_RULES
133
+ prompt = pb.build_system_prompt(Path("."), is_offline=True, complexity="high", task_context=ctx)
134
+ assert "You are running in OFFLINE mode" in prompt
135
+ assert "Execution & Goal Convergence Rules" not in prompt
136
+ assert len(prompt) < 3500 # Lean offline prompt under 3500 chars (~600 tokens vs 4500 cloud tokens)
@@ -1,70 +0,0 @@
1
- import pytest
2
- from unittest.mock import MagicMock, patch
3
- from codeagent.config import Config
4
- from codeagent.backends.openai_backend import _HTTPXClient, OpenAIBackend
5
-
6
-
7
- def test_httpx_client_uses_http1_for_local_bridge():
8
- """Verify that localhost/127.0.0.1 uses an unbuffered HTTP/1.1 client for llama-server."""
9
- client_local = _HTTPXClient("http://127.0.0.1:8080/v1", "not-needed")
10
- # http2 is disabled for local server
11
- assert client_local.http_client._transport._pool._http2 is False or not getattr(client_local.http_client, "_http2", True)
12
-
13
- client_remote = _HTTPXClient("https://test.169pi.ai/v1", "not-needed")
14
- assert client_remote.http_client is not None
15
-
16
-
17
- def test_force_no_thinking_flag_on_config():
18
- """Verify that force_no_thinking flag defaults to False and can be set to True."""
19
- cfg = Config()
20
- assert cfg.force_no_thinking is False
21
-
22
- cfg_no_think = Config(force_no_thinking=True)
23
- assert cfg_no_think.force_no_thinking is True
24
-
25
-
26
- def test_openai_backend_suppresses_think_in_non_thinking_mode():
27
- """Verify that OpenAIBackend suppresses think tokens when enable_thinking=False."""
28
- cfg = Config(base_url="http://127.0.0.1:8080/v1")
29
- backend = OpenAIBackend(cfg)
30
-
31
- # Mock response chunks containing a <think> block
32
- class MockDelta:
33
- def __init__(self, content=None, tool_calls=None):
34
- self.content = content
35
- self.tool_calls = tool_calls
36
-
37
- class MockChoice:
38
- def __init__(self, delta):
39
- self.delta = delta
40
-
41
- class MockChunk:
42
- def __init__(self, content):
43
- self.choices = [MockChoice(MockDelta(content=content))]
44
-
45
- mock_stream = [
46
- MockChunk("<think>"),
47
- MockChunk("Reasoning about prime numbers..."),
48
- MockChunk("</think>"),
49
- MockChunk("def is_prime(n):"),
50
- MockChunk(" return n > 1"),
51
- ]
52
-
53
- mock_client = MagicMock()
54
- mock_client.chat.completions.create.return_value = mock_stream
55
- backend._client = mock_client
56
- backend._resolved_model = "alpie_9b"
57
-
58
- events = list(backend.chat_completion_stream(
59
- messages=[{"role": "user", "content": "Write a prime checker"}],
60
- enable_thinking=False,
61
- ))
62
-
63
- # In non-thinking mode, thinking_start and thinking_delta must NOT be emitted
64
- event_types = [e[0] for e in events]
65
- assert "thinking_start" not in event_types
66
- assert "thinking_delta" not in event_types
67
-
68
- # Only real content tokens must be emitted
69
- token_events = [e[1]["delta"] for e in events if e[0] == "token"]
70
- assert "".join(token_events).strip() == "def is_prime(n): return n > 1"
File without changes
File without changes