alpiecode 10.6.6__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 (58) hide show
  1. {alpiecode-10.6.6 → alpiecode-10.6.9}/PKG-INFO +1 -1
  2. {alpiecode-10.6.6 → alpiecode-10.6.9}/pyproject.toml +1 -1
  3. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/agent.py +18 -7
  7. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/backends/local_backend.py +271 -1
  8. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/backends/openai_backend.py +66 -7
  9. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/cli.py +3 -0
  10. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/config.py +21 -1
  11. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/local_model.py +4 -1
  12. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/orchestrator.py +15 -5
  13. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/prompt.py +14 -1
  14. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/rephraser.py +7 -1
  15. alpiecode-10.6.9/test/test_non_thinking_and_local_stream.py +136 -0
  16. {alpiecode-10.6.6 → alpiecode-10.6.9}/README.md +0 -0
  17. {alpiecode-10.6.6 → alpiecode-10.6.9}/setup.cfg +0 -0
  18. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode/__init__.py +0 -0
  19. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  20. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/entry_points.txt +0 -0
  21. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/requires.txt +0 -0
  22. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/alpiecode.egg-info/top_level.txt +0 -0
  23. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/assessment.py +0 -0
  24. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/backends/__init__.py +0 -0
  25. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/backends/base.py +0 -0
  26. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/cache.py +0 -0
  27. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/client.py +0 -0
  28. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/compaction.py +0 -0
  29. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/context.py +0 -0
  30. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/discovery.py +0 -0
  31. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/doctor.py +0 -0
  32. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/dream_rsi.py +0 -0
  33. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/executor.py +0 -0
  34. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/extension/alpiecode.vsix +0 -0
  35. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/git_ops.py +0 -0
  36. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/github.py +0 -0
  37. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/guardian.py +0 -0
  38. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/guardrails.py +0 -0
  39. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/ipython_ext.py +0 -0
  40. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/media.py +0 -0
  41. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/memory.py +0 -0
  42. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/metrics.py +0 -0
  43. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/paths.py +0 -0
  44. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/progress.py +0 -0
  45. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/tools.py +0 -0
  49. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/updater.py +0 -0
  50. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/verifier.py +0 -0
  51. {alpiecode-10.6.6 → alpiecode-10.6.9}/src/codeagent/vscode_installer.py +0 -0
  52. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_adaptive_resilience.py +0 -0
  53. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_cli_ui.py +0 -0
  54. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_context_and_trajectory.py +0 -0
  55. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_dream_rsi.py +0 -0
  56. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_local_model_download.py +0 -0
  57. {alpiecode-10.6.6 → alpiecode-10.6.9}/test/test_task_assessment.py +0 -0
  58. {alpiecode-10.6.6 → 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.6
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.6"
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.6
3
+ Version: 10.6.9
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -51,5 +51,6 @@ test/test_cli_ui.py
51
51
  test/test_context_and_trajectory.py
52
52
  test/test_dream_rsi.py
53
53
  test/test_local_model_download.py
54
+ test/test_non_thinking_and_local_stream.py
54
55
  test/test_task_assessment.py
55
56
  test/test_verifier.py
@@ -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.6"
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."""
@@ -376,12 +376,16 @@ def run_agent(
376
376
  else:
377
377
  status_spinner.update("Reasoning through implementation...", style="bold magenta")
378
378
 
379
- elif event.type == "thinking_delta" and verbose and debug:
380
- status_spinner.pause()
379
+ elif event.type == "thinking_delta" and verbose:
381
380
  delta = event.data.get("delta", "")
382
- if delta:
383
- sys.stdout.write(delta)
384
- sys.stdout.flush()
381
+ count = event.data.get("count", 0)
382
+ if debug:
383
+ if delta:
384
+ status_spinner.pause()
385
+ sys.stdout.write(delta)
386
+ sys.stdout.flush()
387
+ elif count > 0 and count % 15 == 0:
388
+ status_spinner.update(f"Reasoning through implementation ({count} tokens)...", style="bold magenta")
385
389
 
386
390
  elif event.type == "thinking_end" and verbose:
387
391
  duration = event.data.get("duration", 0)
@@ -504,7 +508,9 @@ def run_agent(
504
508
  else:
505
509
  print(f"── Step {current_turn} ──")
506
510
  if current_turn > 0:
507
- status_spinner.resume(f"Step {current_turn}: Thinking...", style="bold cyan")
511
+ is_turn_thinking = event.data.get("thinking", True)
512
+ turn_label = "Thinking..." if is_turn_thinking else "Working..."
513
+ status_spinner.resume(f"Step {current_turn}: {turn_label}", style="bold cyan")
508
514
 
509
515
  elif event.type == "compaction" and verbose:
510
516
  status_spinner.pause()
@@ -728,7 +734,12 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True, debug: bool = Fal
728
734
  session_mgr = SessionManager()
729
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))
730
736
 
731
- 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
+ )
732
743
  active_tools = PromptBuilder().get_tools(is_offline=is_offline)
733
744
 
734
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):
@@ -175,7 +175,16 @@ class _HTTPXClient:
175
175
  def __init__(self, base_url: str, api_key: str, http_client: Any = None):
176
176
  self.base_url = base_url
177
177
  self.api_key = api_key or "not-needed"
178
- self.http_client = http_client if http_client is not None else get_shared_http_client()
178
+ if http_client is not None:
179
+ self.http_client = http_client
180
+ elif "127.0.0.1" in base_url or "localhost" in base_url:
181
+ self.http_client = httpx.Client(
182
+ http2=False,
183
+ timeout=httpx.Timeout(180.0, connect=5.0, read=180.0, write=30.0),
184
+ limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
185
+ )
186
+ else:
187
+ self.http_client = get_shared_http_client()
179
188
  self.chat = _Namespace(completions=_HTTPXChatCompletions(self))
180
189
  self.models = _HTTPXModels(self)
181
190
 
@@ -284,7 +293,7 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
284
293
  "_parse_error": "Failed to parse streaming JSON arguments",
285
294
  }
286
295
 
287
- 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
288
297
  from .base import ChatResponse, ToolCall
289
298
 
290
299
 
@@ -297,6 +306,18 @@ class OpenAIBackend:
297
306
  self._resolved_model: Optional[str] = None
298
307
  self._max_model_len: Optional[int] = None
299
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
+
300
321
  @property
301
322
  def name(self) -> str:
302
323
  return f"Online API ({self._cfg.base_url})"
@@ -397,6 +418,17 @@ class OpenAIBackend:
397
418
  except ValueError:
398
419
  pass
399
420
 
421
+ # For local endpoints (e.g. llama-server on Android), instruct model to skip <think> in fast mode
422
+ if not enable_thinking and ("127.0.0.1" in getattr(self._cfg, "base_url", "") or "localhost" in getattr(self._cfg, "base_url", "")):
423
+ clean_messages = []
424
+ for m in messages:
425
+ clean_messages.append(dict(m))
426
+ if clean_messages and clean_messages[-1].get("role") == "user":
427
+ u_text = str(clean_messages[-1].get("content", ""))
428
+ if "[Direct Mode:" not in u_text:
429
+ clean_messages[-1]["content"] = u_text + "\n\n[Direct Mode: Output code and tool calls directly without <think> tags. Do not deliberate.]"
430
+ messages = clean_messages
431
+
400
432
  params = {
401
433
  "model": model_name,
402
434
  "messages": messages,
@@ -419,6 +451,8 @@ class OpenAIBackend:
419
451
  response = client.chat.completions.create(**params)
420
452
 
421
453
  in_think = enable_thinking
454
+ suppress_think = not enable_thinking
455
+ inside_suppressed_think = False
422
456
  think_started = False
423
457
  full_content = []
424
458
  full_reasoning = []
@@ -472,16 +506,38 @@ class OpenAIBackend:
472
506
 
473
507
  # Explicit reasoning field from some models
474
508
  explicit_reasoning = getattr(delta, "reasoning", None) or getattr(delta, "reasoning_content", None)
475
- if explicit_reasoning:
509
+ if explicit_reasoning and enable_thinking:
476
510
  if not think_started:
477
511
  think_started = True
478
512
  yield ("thinking_start", {"time": start_time})
479
513
  full_reasoning.append(explicit_reasoning)
480
- yield ("thinking_delta", {"delta": explicit_reasoning})
514
+ yield ("thinking_delta", {"delta": explicit_reasoning, "count": len(full_reasoning)})
481
515
 
482
516
  # Content deltas (contains <think>...</think> on vLLM/Qwen models)
483
517
  if delta.content:
484
518
  text = delta.content
519
+ if suppress_think:
520
+ # In non-thinking mode, swallow any think tags/blocks emitted by local models
521
+ if "<think>" in text:
522
+ inside_suppressed_think = True
523
+ text = text.replace("<think>", "")
524
+ if inside_suppressed_think:
525
+ if "<tool_call>" in text or "DONE:" in text or "<function=" in text:
526
+ inside_suppressed_think = False
527
+ elif "</think>" in text:
528
+ _, after = text.split("</think>", 1)
529
+ inside_suppressed_think = False
530
+ text = after
531
+ else:
532
+ text = ""
533
+ else:
534
+ text = text.replace("</think>", "")
535
+ if not text:
536
+ continue
537
+ full_content.append(text)
538
+ yield ("token", {"delta": text})
539
+ continue
540
+
485
541
  if in_think:
486
542
  if not think_started:
487
543
  think_started = True
@@ -491,7 +547,7 @@ class OpenAIBackend:
491
547
  before, after = text.split("</think>", 1)
492
548
  if before:
493
549
  full_reasoning.append(before)
494
- yield ("thinking_delta", {"delta": before})
550
+ yield ("thinking_delta", {"delta": before, "count": len(full_reasoning)})
495
551
  in_think = False
496
552
  elapsed = time.time() - start_time
497
553
  yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
@@ -503,7 +559,7 @@ class OpenAIBackend:
503
559
  before, after = text.split("DONE:", 1)
504
560
  if before:
505
561
  full_reasoning.append(before)
506
- yield ("thinking_delta", {"delta": before})
562
+ yield ("thinking_delta", {"delta": before, "count": len(full_reasoning)})
507
563
  in_think = False
508
564
  elapsed = time.time() - start_time
509
565
  yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
@@ -513,7 +569,7 @@ class OpenAIBackend:
513
569
  else:
514
570
  clean_text = text.replace("<think>", "")
515
571
  full_reasoning.append(clean_text)
516
- yield ("thinking_delta", {"delta": clean_text})
572
+ yield ("thinking_delta", {"delta": clean_text, "count": len(full_reasoning)})
517
573
  else:
518
574
  full_content.append(text)
519
575
  yield ("token", {"delta": text})
@@ -664,6 +720,9 @@ class OpenAIBackend:
664
720
  reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
665
721
  content = ""
666
722
 
723
+ if not enable_thinking:
724
+ reasoning = None
725
+
667
726
  return ChatResponse(
668
727
  content=content,
669
728
  reasoning=reasoning,
@@ -274,10 +274,13 @@ def main():
274
274
  if getattr(args, "force_thinking", False):
275
275
  cfg.force_thinking = True
276
276
  cfg.enable_thinking = True
277
+ cfg.force_no_thinking = False
277
278
  elif getattr(args, "thinking", False):
278
279
  cfg.enable_thinking = True
280
+ cfg.force_no_thinking = False
279
281
  elif getattr(args, "no_thinking", False):
280
282
  cfg.enable_thinking = False
283
+ cfg.force_no_thinking = True
281
284
 
282
285
  if args.command == "run":
283
286
  if args.max_turns:
@@ -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
 
@@ -152,6 +160,7 @@ class Config:
152
160
  enable_thinking: bool = False
153
161
  adaptive_thinking: bool = True
154
162
  force_thinking: bool = False
163
+ force_no_thinking: bool = False
155
164
  n_ctx: int = 32768
156
165
  n_gpu_layers: Optional[int] = None
157
166
 
@@ -298,6 +307,17 @@ def interactive_init() -> Config:
298
307
  else:
299
308
  print("ℹ️ iOS Inference: Online model endpoint (https://test.169pi.ai) is configured.")
300
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")
301
321
  else:
302
322
  try:
303
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()
@@ -327,7 +332,10 @@ class AgentOrchestrator:
327
332
 
328
333
  # ── Adaptive thinking (base setting) ──
329
334
  strict_format = getattr(getattr(task_context, "assessment", None), "requires_strict_format", False) or getattr(task_context, "requires_strict_format", False)
330
- if getattr(cfg, "force_thinking", False):
335
+ if getattr(cfg, "force_no_thinking", False):
336
+ base_enable_thinking = False
337
+ yield AgentEvent("adaptive_mode", {"message": "Non-thinking mode explicitly requested via --no-thinking; fast execution enabled.", "thinking": False})
338
+ elif getattr(cfg, "force_thinking", False):
331
339
  base_enable_thinking = True
332
340
  elif strict_format:
333
341
  base_enable_thinking = False
@@ -400,7 +408,9 @@ class AgentOrchestrator:
400
408
  )
401
409
 
402
410
  # ── Turn-Level Adaptive Reasoning Dynamics ──
403
- if getattr(cfg, "force_thinking", False) or not getattr(cfg, "adaptive_thinking", True):
411
+ if getattr(cfg, "force_no_thinking", False):
412
+ turn_enable_thinking = False
413
+ elif getattr(cfg, "force_thinking", False) or not getattr(cfg, "adaptive_thinking", True):
404
414
  turn_enable_thinking = base_enable_thinking
405
415
  elif strict_format:
406
416
  if prior_turn_had_error:
@@ -440,7 +450,7 @@ class AgentOrchestrator:
440
450
  yield AgentEvent("safety_ceiling", {"turn": turn, "ceiling": safety_ceiling})
441
451
  break
442
452
 
443
- yield AgentEvent("turn_start", {"turn": turn})
453
+ yield AgentEvent("turn_start", {"turn": turn, "thinking": turn_enable_thinking})
444
454
 
445
455
  # In offline mode, guide the model with current workspace progress
446
456
  if is_offline and turn >= 1:
@@ -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)
File without changes
File without changes