alpiecode 10.6.4__tar.gz → 10.6.6__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.
- {alpiecode-10.6.4 → alpiecode-10.6.6}/PKG-INFO +1 -1
- {alpiecode-10.6.4 → alpiecode-10.6.6}/pyproject.toml +1 -1
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/local_backend.py +243 -9
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_local_model_download.py +97 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/README.md +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/setup.cfg +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/agent.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/assessment.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/cache.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/cli.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/client.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/config.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/context.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/dream_rsi.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/executor.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/github.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/local_model.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/media.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/memory.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/orchestrator.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/paths.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/progress.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/prompt.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/server.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/session.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/system1.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/tools.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/updater.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/verifier.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_adaptive_resilience.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_cli_ui.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_context_and_trajectory.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_dream_rsi.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_task_assessment.py +0 -0
- {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_verifier.py +0 -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.
|
|
20
|
+
__version__ = "10.6.6"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
import subprocess
|
|
3
4
|
import sys
|
|
4
|
-
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
8
|
|
|
6
9
|
from ..config import Config
|
|
7
10
|
from ..local_model import LocalModel
|
|
8
|
-
from ..paths import is_android, is_ios
|
|
11
|
+
from ..paths import ensure_dir, get_alpiecode_dir, is_android, is_ios
|
|
9
12
|
from .base import ChatResponse, ToolCall
|
|
10
13
|
|
|
11
14
|
|
|
@@ -425,7 +428,7 @@ class iOSBackend(BasePlatformBackend):
|
|
|
425
428
|
import llama_cpp
|
|
426
429
|
return True
|
|
427
430
|
except ImportError:
|
|
428
|
-
return self._is_bridge_online()
|
|
431
|
+
return self._is_bridge_online() or self._is_wasm_available()
|
|
429
432
|
|
|
430
433
|
def _get_bridge_url(self) -> str:
|
|
431
434
|
return os.environ.get("IOS_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
|
|
@@ -438,6 +441,224 @@ class iOSBackend(BasePlatformBackend):
|
|
|
438
441
|
except Exception:
|
|
439
442
|
return False
|
|
440
443
|
|
|
444
|
+
def _get_wasm_binary_path(self) -> Optional[Path]:
|
|
445
|
+
"""Locate the llama.wasm binary for a-Shell / WASI execution."""
|
|
446
|
+
custom = os.environ.get("ALPIECODE_WASM_PATH") or os.environ.get("LLAMA_WASM_PATH")
|
|
447
|
+
if custom and Path(custom).exists():
|
|
448
|
+
return Path(custom)
|
|
449
|
+
candidates = [
|
|
450
|
+
get_alpiecode_dir() / "bin" / "llama.wasm",
|
|
451
|
+
get_alpiecode_dir() / "bin" / "llama-cli.wasm",
|
|
452
|
+
Path.home() / "Documents" / ".alpiecode" / "bin" / "llama.wasm",
|
|
453
|
+
Path.home() / ".alpiecode" / "bin" / "llama.wasm",
|
|
454
|
+
Path(sys.prefix) / "bin" / "llama.wasm",
|
|
455
|
+
Path.cwd() / "llama.wasm",
|
|
456
|
+
Path.cwd() / ".alpiecode" / "bin" / "llama.wasm",
|
|
457
|
+
]
|
|
458
|
+
for c in candidates:
|
|
459
|
+
if c.is_file():
|
|
460
|
+
return c
|
|
461
|
+
w_bin = shutil.which("llama.wasm") or shutil.which("llama-cli.wasm")
|
|
462
|
+
if w_bin:
|
|
463
|
+
return Path(w_bin)
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
def _get_model_path_safe(self) -> Path:
|
|
467
|
+
"""Resolve model path whether _ensure_model returns LocalModel or Path."""
|
|
468
|
+
model_obj = self._ensure_model()
|
|
469
|
+
if hasattr(model_obj, "ensure_model"):
|
|
470
|
+
return model_obj.ensure_model()
|
|
471
|
+
if hasattr(model_obj, "model_path") and model_obj.model_path:
|
|
472
|
+
return Path(model_obj.model_path)
|
|
473
|
+
return Path(model_obj)
|
|
474
|
+
|
|
475
|
+
def _is_wasm_available(self) -> bool:
|
|
476
|
+
"""Check if WASM runner (wasm command or executable .wasm) is available in a-Shell."""
|
|
477
|
+
has_wasm_cmd = shutil.which("wasm") is not None
|
|
478
|
+
wasm_bin = self._get_wasm_binary_path()
|
|
479
|
+
return bool(has_wasm_cmd or wasm_bin)
|
|
480
|
+
|
|
481
|
+
def _ensure_wasm_runner(self) -> Tuple[str, Path]:
|
|
482
|
+
"""Verify WASM binary and return (wasm_command, wasm_binary_path)."""
|
|
483
|
+
wasm_bin = self._get_wasm_binary_path()
|
|
484
|
+
bin_dir = ensure_dir(get_alpiecode_dir() / "bin")
|
|
485
|
+
default_wasm = bin_dir / "llama.wasm"
|
|
486
|
+
|
|
487
|
+
if not wasm_bin:
|
|
488
|
+
# Attempt automatic download from HuggingFace so user doesn't have to manually transfer files
|
|
489
|
+
try:
|
|
490
|
+
from huggingface_hub import hf_hub_download
|
|
491
|
+
repo = getattr(self._cfg, "model_repo", "169Pi/alpie_learn_gguf")
|
|
492
|
+
downloaded = hf_hub_download(
|
|
493
|
+
repo_id=repo,
|
|
494
|
+
filename="llama.wasm",
|
|
495
|
+
local_dir=str(bin_dir),
|
|
496
|
+
)
|
|
497
|
+
if downloaded and Path(downloaded).exists():
|
|
498
|
+
wasm_bin = Path(downloaded)
|
|
499
|
+
except Exception:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
if not wasm_bin:
|
|
503
|
+
raise RuntimeError(
|
|
504
|
+
f"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
505
|
+
f"│ a-Shell WebAssembly Inference: │\n"
|
|
506
|
+
f"│ llama.wasm binary not found. │\n"
|
|
507
|
+
f"│ │\n"
|
|
508
|
+
f"│ To enable offline inference in a-Shell: │\n"
|
|
509
|
+
f"│ 1. Place 'llama.wasm' into: │\n"
|
|
510
|
+
f"│ {str(default_wasm)[:55]:<55}│\n"
|
|
511
|
+
f"│ 2. Or set: export ALPIECODE_WASM_PATH=/path/to/llama.wasm │\n"
|
|
512
|
+
f"╰────────────────────────────────────────────────────────────╯"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
runner = shutil.which("wasm") or "wasm"
|
|
516
|
+
return runner, wasm_bin
|
|
517
|
+
|
|
518
|
+
def _format_messages_to_prompt(self, messages: List[dict]) -> str:
|
|
519
|
+
"""Format OpenAI-style message dictionary list into ChatML prompt string."""
|
|
520
|
+
sanitized = self._sanitize_messages_for_gguf(messages)
|
|
521
|
+
parts = []
|
|
522
|
+
for msg in sanitized:
|
|
523
|
+
role = msg.get("role", "user")
|
|
524
|
+
content = msg.get("content", "")
|
|
525
|
+
parts.append(f"<|im_start|>{role}\n{content}<|im_end|>")
|
|
526
|
+
parts.append("<|im_start|>assistant\n")
|
|
527
|
+
return "\n".join(parts)
|
|
528
|
+
|
|
529
|
+
def _wasm_chat_completion(
|
|
530
|
+
self,
|
|
531
|
+
messages: List[dict],
|
|
532
|
+
tools: Optional[List[dict]] = None,
|
|
533
|
+
temperature: float = 0.2,
|
|
534
|
+
max_tokens: int = 2048,
|
|
535
|
+
enable_thinking: bool = True,
|
|
536
|
+
) -> ChatResponse:
|
|
537
|
+
runner, wasm_path = self._ensure_wasm_runner()
|
|
538
|
+
model_path = self._get_model_path_safe()
|
|
539
|
+
prompt = self._format_messages_to_prompt(messages)
|
|
540
|
+
|
|
541
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".txt") as tf:
|
|
542
|
+
tf.write(prompt)
|
|
543
|
+
prompt_file = tf.name
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
n_ctx = str(getattr(self._cfg, "n_ctx", 4096))
|
|
547
|
+
base_args = [
|
|
548
|
+
"-m", str(model_path),
|
|
549
|
+
"-f", prompt_file,
|
|
550
|
+
"-n", str(max_tokens),
|
|
551
|
+
"--temp", str(temperature),
|
|
552
|
+
"-c", n_ctx,
|
|
553
|
+
"--no-mmap",
|
|
554
|
+
"--simple-io",
|
|
555
|
+
"--no-display-prompt",
|
|
556
|
+
"-t", "4",
|
|
557
|
+
]
|
|
558
|
+
if shutil.which(runner):
|
|
559
|
+
cmd = [runner, str(wasm_path)] + base_args
|
|
560
|
+
else:
|
|
561
|
+
cmd = [str(wasm_path)] + base_args
|
|
562
|
+
|
|
563
|
+
res = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
564
|
+
if res.returncode != 0:
|
|
565
|
+
err = res.stderr.strip() or res.stdout.strip()
|
|
566
|
+
if "memory" in err.lower() or "bounds" in err.lower() or "alloc" in err.lower():
|
|
567
|
+
raise RuntimeError(
|
|
568
|
+
f"\n❌ WASM Memory Allocation Failed for '{model_path.name}'.\n"
|
|
569
|
+
f" The model size ({model_path.stat().st_size / (1024**3):.2f} GB) exceeded the 32-bit WASM limit (~2-4 GB).\n"
|
|
570
|
+
f" Details: {err[:200]}"
|
|
571
|
+
)
|
|
572
|
+
raise RuntimeError(f"WASM llama execution failed (code {res.returncode}): {err[:250]}")
|
|
573
|
+
|
|
574
|
+
raw_output = res.stdout.strip()
|
|
575
|
+
content = raw_output
|
|
576
|
+
reasoning = None
|
|
577
|
+
if "<think>" in raw_output and "</think>" in raw_output:
|
|
578
|
+
p1, p2 = raw_output.split("</think>", 1)
|
|
579
|
+
reasoning = p1.replace("<think>", "").strip()
|
|
580
|
+
content = p2.strip()
|
|
581
|
+
elif "<think>" in raw_output:
|
|
582
|
+
content = raw_output.replace("<think>", "").strip()
|
|
583
|
+
|
|
584
|
+
return ChatResponse(content=content, reasoning=reasoning, tool_calls=None, raw={"output": raw_output})
|
|
585
|
+
finally:
|
|
586
|
+
try:
|
|
587
|
+
os.remove(prompt_file)
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
def _wasm_chat_completion_stream(
|
|
592
|
+
self,
|
|
593
|
+
messages: List[dict],
|
|
594
|
+
tools: Optional[List[dict]] = None,
|
|
595
|
+
temperature: Optional[float] = None,
|
|
596
|
+
max_tokens: Optional[int] = None,
|
|
597
|
+
enable_thinking: bool = True,
|
|
598
|
+
):
|
|
599
|
+
runner, wasm_path = self._ensure_wasm_runner()
|
|
600
|
+
model_path = self._get_model_path_safe()
|
|
601
|
+
prompt = self._format_messages_to_prompt(messages)
|
|
602
|
+
|
|
603
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".txt") as tf:
|
|
604
|
+
tf.write(prompt)
|
|
605
|
+
prompt_file = tf.name
|
|
606
|
+
|
|
607
|
+
proc = None
|
|
608
|
+
try:
|
|
609
|
+
n_ctx = str(getattr(self._cfg, "n_ctx", 4096))
|
|
610
|
+
t_val = str(temperature if temperature is not None else 0.2)
|
|
611
|
+
m_val = str(max_tokens if max_tokens is not None else 2048)
|
|
612
|
+
|
|
613
|
+
base_args = [
|
|
614
|
+
"-m", str(model_path),
|
|
615
|
+
"-f", prompt_file,
|
|
616
|
+
"-n", m_val,
|
|
617
|
+
"--temp", t_val,
|
|
618
|
+
"-c", n_ctx,
|
|
619
|
+
"--no-mmap",
|
|
620
|
+
"--simple-io",
|
|
621
|
+
"--no-display-prompt",
|
|
622
|
+
"-t", "4",
|
|
623
|
+
]
|
|
624
|
+
if shutil.which(runner):
|
|
625
|
+
cmd = [runner, str(wasm_path)] + base_args
|
|
626
|
+
else:
|
|
627
|
+
cmd = [str(wasm_path)] + base_args
|
|
628
|
+
|
|
629
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
630
|
+
full_content = []
|
|
631
|
+
for line in proc.stdout:
|
|
632
|
+
if line:
|
|
633
|
+
full_content.append(line)
|
|
634
|
+
yield ("token", {"delta": line})
|
|
635
|
+
proc.wait()
|
|
636
|
+
if proc.returncode != 0:
|
|
637
|
+
err = proc.stderr.read().strip() if proc.stderr else ""
|
|
638
|
+
if "memory" in err.lower() or "bounds" in err.lower():
|
|
639
|
+
raise RuntimeError(
|
|
640
|
+
f"\n❌ WASM Memory Allocation Failed for '{model_path.name}'.\n"
|
|
641
|
+
f" Details: {err[:200]}"
|
|
642
|
+
)
|
|
643
|
+
if not full_content:
|
|
644
|
+
raise RuntimeError(f"WASM execution exited with code {proc.returncode}: {err[:200]}")
|
|
645
|
+
|
|
646
|
+
yield ("done", ChatResponse(
|
|
647
|
+
content="".join(full_content).strip(),
|
|
648
|
+
reasoning=None,
|
|
649
|
+
tool_calls=None,
|
|
650
|
+
))
|
|
651
|
+
finally:
|
|
652
|
+
if proc and proc.poll() is None:
|
|
653
|
+
try:
|
|
654
|
+
proc.kill()
|
|
655
|
+
except Exception:
|
|
656
|
+
pass
|
|
657
|
+
try:
|
|
658
|
+
os.remove(prompt_file)
|
|
659
|
+
except Exception:
|
|
660
|
+
pass
|
|
661
|
+
|
|
441
662
|
def chat_completion(
|
|
442
663
|
self,
|
|
443
664
|
messages: List[dict],
|
|
@@ -455,12 +676,18 @@ class iOSBackend(BasePlatformBackend):
|
|
|
455
676
|
if self._is_bridge_online():
|
|
456
677
|
return self._bridge_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
|
|
457
678
|
|
|
679
|
+
if self._is_wasm_available():
|
|
680
|
+
return self._wasm_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
|
|
681
|
+
|
|
458
682
|
raise RuntimeError(
|
|
459
683
|
"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
460
|
-
"│ iOS Native Inference:
|
|
684
|
+
"│ iOS Native Inference (a-Shell / Container): │\n"
|
|
461
685
|
"│ Dynamic compilation via pip is disabled on iOS. │\n"
|
|
462
|
-
"│
|
|
463
|
-
"│
|
|
686
|
+
"│ │\n"
|
|
687
|
+
"│ Choose one of the following to run offline on iOS: │\n"
|
|
688
|
+
"│ 1. a-Shell WASM: Place llama.wasm into ~/.alpiecode/bin/ │\n"
|
|
689
|
+
"│ 2. Local Bridge: Run local server on 127.0.0.1:8080 │\n"
|
|
690
|
+
"│ 3. Pre-bundled: llama.cpp inside iOS app container │\n"
|
|
464
691
|
"╰────────────────────────────────────────────────────────────╯"
|
|
465
692
|
)
|
|
466
693
|
|
|
@@ -527,12 +754,19 @@ class iOSBackend(BasePlatformBackend):
|
|
|
527
754
|
yield from self._bridge_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
|
|
528
755
|
return
|
|
529
756
|
|
|
757
|
+
if self._is_wasm_available():
|
|
758
|
+
yield from self._wasm_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
|
|
759
|
+
return
|
|
760
|
+
|
|
530
761
|
raise RuntimeError(
|
|
531
762
|
"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
532
|
-
"│ iOS Native Inference:
|
|
763
|
+
"│ iOS Native Inference (a-Shell / Container): │\n"
|
|
533
764
|
"│ Dynamic compilation via pip is disabled on iOS. │\n"
|
|
534
|
-
"│
|
|
535
|
-
"│
|
|
765
|
+
"│ │\n"
|
|
766
|
+
"│ Choose one of the following to run offline on iOS: │\n"
|
|
767
|
+
"│ 1. a-Shell WASM: Place llama.wasm into ~/.alpiecode/bin/ │\n"
|
|
768
|
+
"│ 2. Local Bridge: Run local server on 127.0.0.1:8080 │\n"
|
|
769
|
+
"│ 3. Pre-bundled: llama.cpp inside iOS app container │\n"
|
|
536
770
|
"╰────────────────────────────────────────────────────────────╯"
|
|
537
771
|
)
|
|
538
772
|
|
|
@@ -286,3 +286,100 @@ def test_embedded_hf_token_used_when_no_env(monkeypatch):
|
|
|
286
286
|
from codeagent.local_model import EMBEDDED_HF_TOKEN
|
|
287
287
|
assert EMBEDDED_HF_TOKEN in captured_tokens
|
|
288
288
|
|
|
289
|
+
|
|
290
|
+
def test_ios_backend_wasm_dispatch(tmp_path, monkeypatch):
|
|
291
|
+
"""When llama_cpp is absent and bridge is offline, iOSBackend dispatches to WASM runner."""
|
|
292
|
+
from codeagent.backends.local_backend import iOSBackend
|
|
293
|
+
cfg = load_config()
|
|
294
|
+
b = iOSBackend(cfg)
|
|
295
|
+
|
|
296
|
+
wasm_bin = tmp_path / "llama.wasm"
|
|
297
|
+
wasm_bin.touch()
|
|
298
|
+
fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
|
|
299
|
+
fake_model.touch()
|
|
300
|
+
|
|
301
|
+
monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
|
|
302
|
+
|
|
303
|
+
mock_run = MagicMock()
|
|
304
|
+
mock_run.returncode = 0
|
|
305
|
+
mock_run.stdout = "<think>Analyzing code...</think>\nHello from WASM offline!"
|
|
306
|
+
mock_run.stderr = ""
|
|
307
|
+
|
|
308
|
+
with patch.dict("sys.modules", {"llama_cpp": None}), \
|
|
309
|
+
patch.object(b, "_is_bridge_online", return_value=False), \
|
|
310
|
+
patch.object(b, "_ensure_model", return_value=fake_model), \
|
|
311
|
+
patch("shutil.which", return_value="/usr/bin/wasm"), \
|
|
312
|
+
patch("subprocess.run", return_value=mock_run) as run_spy:
|
|
313
|
+
res = b.chat_completion([{"role": "user", "content": "hello"}])
|
|
314
|
+
assert res.content == "Hello from WASM offline!"
|
|
315
|
+
assert res.reasoning == "Analyzing code..."
|
|
316
|
+
|
|
317
|
+
# Verify command arguments
|
|
318
|
+
assert run_spy.called
|
|
319
|
+
cmd = run_spy.call_args[0][0]
|
|
320
|
+
assert cmd[0] == "/usr/bin/wasm"
|
|
321
|
+
assert cmd[1] == str(wasm_bin)
|
|
322
|
+
assert "-m" in cmd
|
|
323
|
+
assert str(fake_model) in cmd
|
|
324
|
+
assert "-f" in cmd
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_ios_backend_wasm_streaming(tmp_path, monkeypatch):
|
|
328
|
+
"""iOSBackend streams tokens line-by-line via WASM runner."""
|
|
329
|
+
from codeagent.backends.local_backend import iOSBackend
|
|
330
|
+
cfg = load_config()
|
|
331
|
+
b = iOSBackend(cfg)
|
|
332
|
+
|
|
333
|
+
wasm_bin = tmp_path / "llama.wasm"
|
|
334
|
+
wasm_bin.touch()
|
|
335
|
+
fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
|
|
336
|
+
fake_model.touch()
|
|
337
|
+
|
|
338
|
+
monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
|
|
339
|
+
|
|
340
|
+
mock_proc = MagicMock()
|
|
341
|
+
mock_proc.stdout = ["Hello ", "from ", "stream!"]
|
|
342
|
+
mock_proc.returncode = 0
|
|
343
|
+
mock_proc.poll.return_value = 0
|
|
344
|
+
|
|
345
|
+
with patch.dict("sys.modules", {"llama_cpp": None}), \
|
|
346
|
+
patch.object(b, "_is_bridge_online", return_value=False), \
|
|
347
|
+
patch.object(b, "_ensure_model", return_value=fake_model), \
|
|
348
|
+
patch("shutil.which", return_value="/usr/bin/wasm"), \
|
|
349
|
+
patch("subprocess.Popen", return_value=mock_proc):
|
|
350
|
+
stream = list(b.chat_completion_stream([{"role": "user", "content": "hi"}]))
|
|
351
|
+
tokens = [payload["delta"] for tag, payload in stream if tag == "token"]
|
|
352
|
+
assert "".join(tokens) == "Hello from stream!"
|
|
353
|
+
assert stream[-1][0] == "done"
|
|
354
|
+
assert stream[-1][1].content == "Hello from stream!"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_ios_backend_wasm_memory_error(tmp_path, monkeypatch):
|
|
358
|
+
"""WASM runtime memory allocation failure throws actionable diagnostic."""
|
|
359
|
+
from codeagent.backends.local_backend import iOSBackend
|
|
360
|
+
cfg = load_config()
|
|
361
|
+
b = iOSBackend(cfg)
|
|
362
|
+
|
|
363
|
+
wasm_bin = tmp_path / "llama.wasm"
|
|
364
|
+
wasm_bin.touch()
|
|
365
|
+
fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
|
|
366
|
+
fake_model.touch()
|
|
367
|
+
|
|
368
|
+
monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
|
|
369
|
+
|
|
370
|
+
mock_run = MagicMock()
|
|
371
|
+
mock_run.returncode = 1
|
|
372
|
+
mock_run.stdout = ""
|
|
373
|
+
mock_run.stderr = "RuntimeError: out of bounds memory access / memory allocation failed"
|
|
374
|
+
|
|
375
|
+
with patch.dict("sys.modules", {"llama_cpp": None}), \
|
|
376
|
+
patch.object(b, "_is_bridge_online", return_value=False), \
|
|
377
|
+
patch.object(b, "_ensure_model", return_value=fake_model), \
|
|
378
|
+
patch("shutil.which", return_value="/usr/bin/wasm"), \
|
|
379
|
+
patch("subprocess.run", return_value=mock_run):
|
|
380
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
381
|
+
b.chat_completion([{"role": "user", "content": "hello"}])
|
|
382
|
+
assert "WASM Memory Allocation Failed" in str(exc_info.value)
|
|
383
|
+
assert "32-bit WASM limit" in str(exc_info.value)
|
|
384
|
+
|
|
385
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|