hypercli-cli 2026.3.13__tar.gz → 2026.3.22__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.
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/.gitignore +3 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/PKG-INFO +4 -4
- hypercli_cli-2026.3.22/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/agent.py +161 -91
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/agents.py +69 -40
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/cli.py +3 -1
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/embed.py +3 -3
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/instances.py +28 -1
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/voice.py +11 -5
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/pyproject.toml +4 -4
- hypercli_cli-2026.3.22/tests/test_agent_env_resolution.py +39 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/tests/test_exec_shell_dryrun.py +46 -0
- hypercli_cli-2026.3.22/tests/test_openclaw_config.py +63 -0
- hypercli_cli-2026.3.13/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/README.md +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/output.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/user.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/hypercli_cli/wallet.py +0 -0
- {hypercli_cli-2026.3.13 → hypercli_cli-2026.3.22}/tests/test_jobs_list_tags.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version: 2026.3.
|
|
3
|
+
Version: 2026.3.22
|
|
4
4
|
Summary: CLI for HyperCLI - GPU orchestration and LLM API
|
|
5
5
|
Project-URL: Homepage, https://hypercli.com
|
|
6
6
|
Project-URL: Documentation, https://docs.hypercli.com
|
|
@@ -9,7 +9,7 @@ Author-email: HyperCLI <support@hypercli.com>
|
|
|
9
9
|
License: MIT
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: httpx>=0.27.0
|
|
12
|
-
Requires-Dist: hypercli-sdk>=2026.3.
|
|
12
|
+
Requires-Dist: hypercli-sdk>=2026.3.22
|
|
13
13
|
Requires-Dist: mutagen>=1.47.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: rich>=14.2.0
|
|
@@ -19,11 +19,11 @@ Provides-Extra: all
|
|
|
19
19
|
Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
|
|
20
20
|
Requires-Dist: eth-account>=0.13.0; extra == 'all'
|
|
21
21
|
Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
|
|
22
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.
|
|
22
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.22; extra == 'all'
|
|
23
23
|
Requires-Dist: web3>=7.0.0; extra == 'all'
|
|
24
24
|
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
|
|
25
25
|
Provides-Extra: comfyui
|
|
26
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.
|
|
26
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.22; extra == 'comfyui'
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2026.3.22"
|
|
@@ -55,6 +55,8 @@ HYPERCLI_DIR = Path.home() / ".hypercli"
|
|
|
55
55
|
AGENT_KEY_PATH = HYPERCLI_DIR / "agent-key.json"
|
|
56
56
|
DEV_API_BASE = "https://api.dev.hypercli.com"
|
|
57
57
|
PROD_API_BASE = "https://api.hypercli.com"
|
|
58
|
+
DEV_INFERENCE_API_BASE = "https://api.agents.dev.hypercli.com"
|
|
59
|
+
PROD_INFERENCE_API_BASE = "https://api.agents.hypercli.com"
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def require_x402_deps():
|
|
@@ -351,7 +353,7 @@ def models(
|
|
|
351
353
|
import httpx
|
|
352
354
|
|
|
353
355
|
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
354
|
-
key = os.getenv("
|
|
356
|
+
key = os.getenv("HYPER_API_KEY")
|
|
355
357
|
headers = {"Authorization": f"Bearer {key}"} if key else {}
|
|
356
358
|
|
|
357
359
|
# Prefer OpenAI-compatible endpoint, then fallback to legacy.
|
|
@@ -372,7 +374,7 @@ def models(
|
|
|
372
374
|
if payload is None:
|
|
373
375
|
console.print(
|
|
374
376
|
f"[red]❌ Failed to fetch models from {urls[0]} or {urls[1]} "
|
|
375
|
-
"(set
|
|
377
|
+
"(set HYPER_API_KEY if endpoint requires auth)[/red]"
|
|
376
378
|
)
|
|
377
379
|
raise typer.Exit(1)
|
|
378
380
|
|
|
@@ -544,12 +546,43 @@ OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
|
544
546
|
|
|
545
547
|
def _resolve_api_base(base_url: str | None = None, dev: bool = False) -> str:
|
|
546
548
|
"""Resolve API base from flag/env, then fall back to dev/prod defaults."""
|
|
547
|
-
return (
|
|
549
|
+
return (
|
|
550
|
+
base_url
|
|
551
|
+
or os.environ.get("HYPER_API_BASE")
|
|
552
|
+
or (DEV_INFERENCE_API_BASE if dev else PROD_INFERENCE_API_BASE)
|
|
553
|
+
).rstrip("/")
|
|
548
554
|
|
|
549
555
|
|
|
550
|
-
def fetch_models(api_key: str, api_base: str =
|
|
556
|
+
def fetch_models(api_key: str, api_base: str = PROD_INFERENCE_API_BASE) -> list[dict]:
|
|
551
557
|
"""Fetch available models from LiteLLM /v1/models (served by HyperClaw)."""
|
|
552
558
|
import httpx
|
|
559
|
+
|
|
560
|
+
def _infer_mode(model_id: str) -> str | None:
|
|
561
|
+
normalized = (model_id or "").strip().lower()
|
|
562
|
+
if "embedding" in normalized:
|
|
563
|
+
return "embedding"
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
def _meta_for_model(model_id: str) -> dict:
|
|
567
|
+
normalized = (model_id or "").strip().lower()
|
|
568
|
+
aliases = {
|
|
569
|
+
"kimi-k2.5": {"name": "Kimi K2.5", "reasoning": True, "contextWindow": 262144},
|
|
570
|
+
"moonshotai/kimi-k2.5": {"name": "Kimi K2.5", "reasoning": True, "contextWindow": 262144},
|
|
571
|
+
"glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
|
|
572
|
+
"zai-org/glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
|
|
573
|
+
"qwen3-embedding-4b": {
|
|
574
|
+
"name": "Qwen3 Embedding 4B",
|
|
575
|
+
"reasoning": False,
|
|
576
|
+
"contextWindow": 32768,
|
|
577
|
+
"mode": "embedding",
|
|
578
|
+
"input": ["text"],
|
|
579
|
+
},
|
|
580
|
+
}
|
|
581
|
+
if normalized in aliases:
|
|
582
|
+
return aliases[normalized]
|
|
583
|
+
suffix = normalized.rsplit("/", 1)[-1]
|
|
584
|
+
return aliases.get(suffix, {})
|
|
585
|
+
|
|
553
586
|
try:
|
|
554
587
|
resp = httpx.get(
|
|
555
588
|
f"{api_base}/v1/models",
|
|
@@ -558,19 +591,14 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
558
591
|
)
|
|
559
592
|
resp.raise_for_status()
|
|
560
593
|
data = resp.json().get("data", [])
|
|
561
|
-
# Known model metadata (context windows, reasoning, etc.)
|
|
562
|
-
MODEL_META = {
|
|
563
|
-
"kimi-k2.5": {"name": "Kimi K2.5", "reasoning": True, "contextWindow": 262144},
|
|
564
|
-
"glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
|
|
565
|
-
}
|
|
566
594
|
return [
|
|
567
595
|
{
|
|
568
596
|
"id": m["id"],
|
|
569
|
-
"name":
|
|
570
|
-
"reasoning":
|
|
571
|
-
"input": ["text"],
|
|
572
|
-
"contextWindow":
|
|
573
|
-
**({"mode": m["mode"]} if m.get("mode") else {}),
|
|
597
|
+
"name": _meta_for_model(m["id"]).get("name", m["id"].replace("-", " ").title()),
|
|
598
|
+
"reasoning": _meta_for_model(m["id"]).get("reasoning", False),
|
|
599
|
+
"input": _meta_for_model(m["id"]).get("input", ["text", "image"]),
|
|
600
|
+
"contextWindow": _meta_for_model(m["id"]).get("contextWindow", 200000),
|
|
601
|
+
**({"mode": m.get("mode") or _meta_for_model(m["id"]).get("mode") or _infer_mode(m["id"])} if (m.get("mode") or _meta_for_model(m["id"]).get("mode") or _infer_mode(m["id"])) else {}),
|
|
574
602
|
}
|
|
575
603
|
for m in data
|
|
576
604
|
if m.get("id")
|
|
@@ -583,16 +611,24 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
583
611
|
"id": "kimi-k2.5",
|
|
584
612
|
"name": "Kimi K2.5",
|
|
585
613
|
"reasoning": True,
|
|
586
|
-
"input": ["text"],
|
|
614
|
+
"input": ["text", "image"],
|
|
587
615
|
"contextWindow": 262144,
|
|
588
616
|
},
|
|
589
617
|
{
|
|
590
618
|
"id": "glm-5",
|
|
591
619
|
"name": "GLM-5",
|
|
592
620
|
"reasoning": True,
|
|
593
|
-
"input": ["text"],
|
|
621
|
+
"input": ["text", "image"],
|
|
594
622
|
"contextWindow": 202752,
|
|
595
623
|
},
|
|
624
|
+
{
|
|
625
|
+
"id": "qwen3-embedding-4b",
|
|
626
|
+
"name": "Qwen3 Embedding 4B",
|
|
627
|
+
"reasoning": False,
|
|
628
|
+
"input": ["text"],
|
|
629
|
+
"contextWindow": 32768,
|
|
630
|
+
"mode": "embedding",
|
|
631
|
+
},
|
|
596
632
|
]
|
|
597
633
|
|
|
598
634
|
|
|
@@ -620,49 +656,21 @@ def openclaw_setup(
|
|
|
620
656
|
console.print("[red]❌ Invalid key file — missing 'key' field[/red]")
|
|
621
657
|
raise typer.Exit(1)
|
|
622
658
|
|
|
623
|
-
|
|
659
|
+
config = {}
|
|
624
660
|
if OPENCLAW_CONFIG_PATH.exists():
|
|
625
661
|
with open(OPENCLAW_CONFIG_PATH) as f:
|
|
626
662
|
config = json.load(f)
|
|
627
|
-
else:
|
|
628
|
-
config = {}
|
|
629
|
-
|
|
630
|
-
# Fetch current model list from LiteLLM via API
|
|
631
|
-
models = fetch_models(api_key)
|
|
632
|
-
|
|
633
|
-
# Patch models.providers.hyperclaw + embedding config
|
|
634
|
-
config.setdefault("models", {}).setdefault("providers", {})
|
|
635
|
-
chat_models = [m for m in models if m.get("mode") != "embedding"]
|
|
636
|
-
embedding_models = [m for m in models if m.get("mode") == "embedding"]
|
|
637
|
-
config["models"]["providers"]["hyperclaw"] = {
|
|
638
|
-
"baseUrl": "https://api.hypercli.com",
|
|
639
|
-
"apiKey": api_key,
|
|
640
|
-
"api": "anthropic-messages",
|
|
641
|
-
"models": chat_models,
|
|
642
|
-
}
|
|
643
|
-
config["models"]["providers"]["hyperclaw-embed"] = {
|
|
644
|
-
"baseUrl": "https://api.hypercli.com/v1",
|
|
645
|
-
"apiKey": api_key,
|
|
646
|
-
"api": "openai-completions",
|
|
647
|
-
"models": embedding_models,
|
|
648
|
-
}
|
|
649
663
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
"
|
|
654
|
-
"model"
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
"
|
|
658
|
-
}
|
|
659
|
-
}
|
|
664
|
+
models = fetch_models(api_key, PROD_INFERENCE_API_BASE)
|
|
665
|
+
snippet = _config_openclaw(api_key, models, PROD_INFERENCE_API_BASE)
|
|
666
|
+
if not default:
|
|
667
|
+
defaults = (((snippet.get("agents") or {}).get("defaults") or {}))
|
|
668
|
+
model_cfg = defaults.get("model") or {}
|
|
669
|
+
model_cfg.pop("primary", None)
|
|
670
|
+
if not model_cfg and "model" in defaults:
|
|
671
|
+
defaults.pop("model", None)
|
|
660
672
|
|
|
661
|
-
|
|
662
|
-
if default:
|
|
663
|
-
config["agents"]["defaults"].setdefault("model", {})
|
|
664
|
-
if chat_models:
|
|
665
|
-
config["agents"]["defaults"]["model"]["primary"] = f"hyperclaw/{chat_models[0]['id']}"
|
|
673
|
+
_deep_merge(config, snippet)
|
|
666
674
|
|
|
667
675
|
# Write back
|
|
668
676
|
OPENCLAW_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -671,15 +679,14 @@ def openclaw_setup(
|
|
|
671
679
|
f.write("\n")
|
|
672
680
|
|
|
673
681
|
console.print(f"[green]✅ Patched {OPENCLAW_CONFIG_PATH}[/green]")
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
console.print("
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
console.print(f" default model: hyperclaw/{chat_models[0]['id']}")
|
|
682
|
+
providers = ((snippet.get("models") or {}).get("providers") or {})
|
|
683
|
+
for provider_id, provider_cfg in providers.items():
|
|
684
|
+
console.print(f" provider: {provider_id} key: {api_key[:16]}...")
|
|
685
|
+
for m in provider_cfg.get("models") or []:
|
|
686
|
+
console.print(f" model: {provider_id}/{m['id']}")
|
|
687
|
+
primary = ((((snippet.get("agents") or {}).get("defaults") or {}).get("model") or {}).get("primary"))
|
|
688
|
+
if primary:
|
|
689
|
+
console.print(f" default model: {primary}")
|
|
683
690
|
console.print("\nOpenClaw will use the Anthropic-compatible /v1/messages endpoint.")
|
|
684
691
|
console.print("Run: [bold]openclaw gateway restart[/bold]")
|
|
685
692
|
|
|
@@ -703,11 +710,43 @@ def _resolve_api_key(key: str | None) -> str:
|
|
|
703
710
|
raise typer.Exit(1)
|
|
704
711
|
|
|
705
712
|
|
|
706
|
-
def _config_openclaw(
|
|
707
|
-
|
|
713
|
+
def _config_openclaw(
|
|
714
|
+
api_key: str,
|
|
715
|
+
models: list[dict],
|
|
716
|
+
api_base: str = PROD_INFERENCE_API_BASE,
|
|
717
|
+
placeholder_env: str | None = None,
|
|
718
|
+
) -> dict:
|
|
719
|
+
"""OpenClaw openclaw.json provider snippet (LLM only)."""
|
|
720
|
+
def _model_suffix(model_id: str) -> str:
|
|
721
|
+
return str(model_id or "").strip().lower().rsplit("/", 1)[-1]
|
|
722
|
+
|
|
723
|
+
def _is_supported_openclaw_model(model: dict) -> bool:
|
|
724
|
+
suffix = _model_suffix(model.get("id", ""))
|
|
725
|
+
return (
|
|
726
|
+
suffix == "glm-5"
|
|
727
|
+
or "kimi" in suffix
|
|
728
|
+
or "embedding" in suffix
|
|
729
|
+
)
|
|
730
|
+
|
|
708
731
|
api_base = api_base.rstrip("/")
|
|
709
|
-
|
|
710
|
-
|
|
732
|
+
supported_models = [m for m in models if _is_supported_openclaw_model(m)]
|
|
733
|
+
chat_models = [m for m in supported_models if m.get("mode") != "embedding"]
|
|
734
|
+
embedding_models = [m for m in supported_models if m.get("mode") == "embedding"]
|
|
735
|
+
kimi_models = [m for m in chat_models if "kimi" in _model_suffix(m.get("id", ""))]
|
|
736
|
+
glm_models = [m for m in chat_models if _model_suffix(m.get("id", "")) == "glm-5"]
|
|
737
|
+
openai_chat_models = [
|
|
738
|
+
m for m in chat_models
|
|
739
|
+
if m not in kimi_models and m not in glm_models
|
|
740
|
+
]
|
|
741
|
+
embedding_model_id = embedding_models[0]["id"] if embedding_models else None
|
|
742
|
+
primary_model = (
|
|
743
|
+
f"kimi-coding/{kimi_models[0]['id']}" if kimi_models else (
|
|
744
|
+
f"hyperclaw/{glm_models[0]['id']}" if glm_models else (
|
|
745
|
+
f"hyperclaw-openai/{openai_chat_models[0]['id']}" if openai_chat_models else None
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
config_api_key = f"${{{placeholder_env}}}" if placeholder_env else api_key
|
|
711
750
|
return {
|
|
712
751
|
"models": {
|
|
713
752
|
"mode": "merge",
|
|
@@ -715,39 +754,69 @@ def _config_openclaw(api_key: str, models: list[dict], api_base: str = PROD_API_
|
|
|
715
754
|
"hyperclaw": {
|
|
716
755
|
# OpenClaw/pi-ai appends /v1/messages for anthropic-messages.
|
|
717
756
|
"baseUrl": api_base,
|
|
718
|
-
"apiKey":
|
|
757
|
+
"apiKey": config_api_key,
|
|
719
758
|
"api": "anthropic-messages",
|
|
720
|
-
"models":
|
|
721
|
-
},
|
|
722
|
-
"hyperclaw-embed": {
|
|
723
|
-
# Embeddings go through the OpenAI-compatible /v1 endpoints.
|
|
724
|
-
"baseUrl": f"{api_base}/v1",
|
|
725
|
-
"apiKey": api_key,
|
|
726
|
-
"api": "openai-completions",
|
|
727
|
-
"models": embedding_models,
|
|
759
|
+
"models": glm_models,
|
|
728
760
|
},
|
|
761
|
+
**(
|
|
762
|
+
{
|
|
763
|
+
"kimi-coding": {
|
|
764
|
+
# Use the upstream Kimi provider semantics while still
|
|
765
|
+
# routing requests through the HyperClaw Anthropic proxy.
|
|
766
|
+
"baseUrl": api_base,
|
|
767
|
+
"apiKey": config_api_key,
|
|
768
|
+
"api": "anthropic-messages",
|
|
769
|
+
"headers": {
|
|
770
|
+
"User-Agent": "claude-code/0.1.0",
|
|
771
|
+
},
|
|
772
|
+
"models": kimi_models,
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
if kimi_models
|
|
776
|
+
else {}
|
|
777
|
+
),
|
|
778
|
+
**(
|
|
779
|
+
{
|
|
780
|
+
"hyperclaw-openai": {
|
|
781
|
+
"baseUrl": f"{api_base}/v1",
|
|
782
|
+
"apiKey": config_api_key,
|
|
783
|
+
"api": "openai-completions",
|
|
784
|
+
"models": openai_chat_models,
|
|
785
|
+
},
|
|
786
|
+
}
|
|
787
|
+
if openai_chat_models
|
|
788
|
+
else {}
|
|
789
|
+
),
|
|
729
790
|
}
|
|
730
791
|
},
|
|
731
792
|
"agents": {
|
|
732
793
|
"defaults": {
|
|
794
|
+
**({"model": {"primary": primary_model}} if primary_model else {}),
|
|
733
795
|
"models": {
|
|
734
|
-
**{f"hyperclaw/{m['id']}": {"alias":
|
|
735
|
-
**{f"
|
|
796
|
+
**{f"hyperclaw/{m['id']}": {"alias": "glm"} for m in glm_models},
|
|
797
|
+
**{f"kimi-coding/{m['id']}": {"alias": "kimi"} for m in kimi_models},
|
|
798
|
+
**{f"hyperclaw-openai/{m['id']}": {"alias": m['id'].split('-')[0]} for m in openai_chat_models},
|
|
736
799
|
},
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
800
|
+
**(
|
|
801
|
+
{
|
|
802
|
+
"memorySearch": {
|
|
803
|
+
"provider": "openai",
|
|
804
|
+
"model": embedding_model_id,
|
|
805
|
+
"remote": {
|
|
806
|
+
"baseUrl": f"{api_base}/v1",
|
|
807
|
+
"apiKey": config_api_key,
|
|
808
|
+
},
|
|
809
|
+
}
|
|
743
810
|
}
|
|
744
|
-
|
|
811
|
+
if embedding_model_id
|
|
812
|
+
else {}
|
|
813
|
+
),
|
|
745
814
|
}
|
|
746
815
|
}
|
|
747
816
|
}
|
|
748
817
|
|
|
749
818
|
|
|
750
|
-
def _config_opencode(api_key: str, models: list[dict], api_base: str =
|
|
819
|
+
def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> dict:
|
|
751
820
|
"""OpenCode opencode.json provider snippet."""
|
|
752
821
|
api_base = api_base.rstrip("/")
|
|
753
822
|
model_entries = {}
|
|
@@ -769,7 +838,7 @@ def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_API_
|
|
|
769
838
|
}
|
|
770
839
|
|
|
771
840
|
|
|
772
|
-
def _config_env(api_key: str, models: list[dict], api_base: str =
|
|
841
|
+
def _config_env(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> str:
|
|
773
842
|
"""Shell env vars for generic OpenAI-compatible tools."""
|
|
774
843
|
api_base = api_base.rstrip("/")
|
|
775
844
|
lines = [
|
|
@@ -786,7 +855,7 @@ def exec_cmd(
|
|
|
786
855
|
command: str = typer.Argument(..., help="Command to execute"),
|
|
787
856
|
timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout (seconds)"),
|
|
788
857
|
):
|
|
789
|
-
"""Execute a command on a
|
|
858
|
+
"""Execute a command on a `hypercli-openclaw` agent container."""
|
|
790
859
|
from . import agents
|
|
791
860
|
|
|
792
861
|
agents.exec_cmd(agent_id=agent_id, command=command, timeout=timeout)
|
|
@@ -796,7 +865,7 @@ def exec_cmd(
|
|
|
796
865
|
def shell_cmd(
|
|
797
866
|
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
798
867
|
):
|
|
799
|
-
"""Open an interactive shell on a
|
|
868
|
+
"""Open an interactive shell on a `hypercli-openclaw` agent container."""
|
|
800
869
|
from . import agents
|
|
801
870
|
|
|
802
871
|
agents.shell(agent_id=agent_id)
|
|
@@ -812,7 +881,8 @@ def config_cmd(
|
|
|
812
881
|
help=f"Output format: {', '.join(FORMAT_CHOICES)}. Omit to show all.",
|
|
813
882
|
),
|
|
814
883
|
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...). Falls back to ~/.hypercli/agent-key.json"),
|
|
815
|
-
base_url: str = typer.Option(None, "--base-url", help="HyperClaw API base URL. Falls back to
|
|
884
|
+
base_url: str = typer.Option(None, "--base-url", help="HyperClaw API base URL. Falls back to HYPER_API_BASE, then --dev/prod defaults"),
|
|
885
|
+
placeholder_env: str = typer.Option(None, "--placeholder-env", help="Write ${ENV_VAR} placeholders into generated config instead of literal API keys"),
|
|
816
886
|
apply: bool = typer.Option(False, "--apply", help="Write config to the appropriate file (openclaw/opencode only)"),
|
|
817
887
|
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
818
888
|
):
|
|
@@ -844,7 +914,7 @@ def config_cmd(
|
|
|
844
914
|
|
|
845
915
|
for fmt in formats:
|
|
846
916
|
if fmt == "openclaw":
|
|
847
|
-
snippet = _config_openclaw(api_key, models, api_base)
|
|
917
|
+
snippet = _config_openclaw(api_key, models, api_base, placeholder_env=placeholder_env)
|
|
848
918
|
_show_snippet("OpenClaw", "~/.openclaw/openclaw.json", snippet, apply, OPENCLAW_CONFIG_PATH)
|
|
849
919
|
elif fmt == "opencode":
|
|
850
920
|
snippet = _config_opencode(api_key, models, api_base)
|
|
@@ -12,14 +12,12 @@ import typer
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
-
from hypercli.agents import Agent, Deployments, OpenClawAgent
|
|
15
|
+
from hypercli.agents import Agent, Deployments, OpenClawAgent, DEFAULT_OPENCLAW_IMAGE
|
|
16
16
|
|
|
17
|
-
app = typer.Typer(help="Manage OpenClaw agent pods
|
|
17
|
+
app = typer.Typer(help="Manage OpenClaw agent pods")
|
|
18
18
|
console = Console()
|
|
19
19
|
PROD_API_BASE = "https://api.hypercli.com"
|
|
20
20
|
DEV_API_BASE = "https://api.dev.hypercli.com"
|
|
21
|
-
PROD_AGENTS_WS_URL = "wss://api.agents.hypercli.com/ws"
|
|
22
|
-
DEV_AGENTS_WS_URL = "wss://api.agents.dev.hypercli.com/ws"
|
|
23
21
|
_GLOBAL_DEV = False
|
|
24
22
|
_GLOBAL_AGENTS_WS_URL: str | None = None
|
|
25
23
|
|
|
@@ -29,6 +27,13 @@ STATE_DIR = Path.home() / ".hypercli"
|
|
|
29
27
|
AGENTS_STATE = STATE_DIR / "agents.json"
|
|
30
28
|
|
|
31
29
|
|
|
30
|
+
def _default_openclaw_image(image: str | None, config: dict | None = None) -> str:
|
|
31
|
+
if image:
|
|
32
|
+
return image
|
|
33
|
+
configured = str((config or {}).get("image") or "").strip()
|
|
34
|
+
return configured or DEFAULT_OPENCLAW_IMAGE
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
@app.callback()
|
|
33
38
|
def agents_root(
|
|
34
39
|
dev: bool = typer.Option(False, "--dev", help="Use the dev HyperClaw agents API"),
|
|
@@ -42,7 +47,10 @@ def agents_root(
|
|
|
42
47
|
|
|
43
48
|
def _get_agent_api_key() -> str:
|
|
44
49
|
"""Resolve HyperClaw API key from env or saved key file."""
|
|
45
|
-
key = os.environ.get("
|
|
50
|
+
key = os.environ.get("HYPER_AGENTS_API_KEY", "").strip()
|
|
51
|
+
if key:
|
|
52
|
+
return key
|
|
53
|
+
key = os.environ.get("HYPER_API_KEY", "").strip()
|
|
46
54
|
if key:
|
|
47
55
|
return key
|
|
48
56
|
if AGENT_KEY_PATH.exists():
|
|
@@ -52,7 +60,7 @@ def _get_agent_api_key() -> str:
|
|
|
52
60
|
if key:
|
|
53
61
|
return key
|
|
54
62
|
console.print("[red]❌ No HyperClaw API key found.[/red]")
|
|
55
|
-
console.print("Set
|
|
63
|
+
console.print("Set HYPER_AGENTS_API_KEY or HYPER_API_KEY, or subscribe: [bold]hyper agent subscribe 1aiu[/bold]")
|
|
56
64
|
raise typer.Exit(1)
|
|
57
65
|
|
|
58
66
|
|
|
@@ -60,12 +68,13 @@ def _get_deployments_client(agents_ws_url: str | None = None) -> Deployments:
|
|
|
60
68
|
"""Create a Deployments client using the HyperClaw API key."""
|
|
61
69
|
from hypercli.http import HTTPClient
|
|
62
70
|
api_key = _get_agent_api_key()
|
|
63
|
-
api_base =
|
|
71
|
+
api_base = (
|
|
72
|
+
os.environ.get("AGENTS_API_BASE_URL")
|
|
73
|
+
or os.environ.get("HYPER_API_BASE")
|
|
74
|
+
or os.environ.get("HYPERCLI_API_URL")
|
|
75
|
+
or (DEV_API_BASE if _GLOBAL_DEV else PROD_API_BASE)
|
|
76
|
+
)
|
|
64
77
|
resolved_agents_ws_url = agents_ws_url or _GLOBAL_AGENTS_WS_URL or os.environ.get("AGENTS_WS_URL")
|
|
65
|
-
if _GLOBAL_DEV and not resolved_agents_ws_url:
|
|
66
|
-
resolved_agents_ws_url = DEV_AGENTS_WS_URL
|
|
67
|
-
if not _GLOBAL_DEV and not resolved_agents_ws_url and os.environ.get("HYPERCLI_API_URL", "").strip() == "":
|
|
68
|
-
resolved_agents_ws_url = PROD_AGENTS_WS_URL
|
|
69
78
|
http = HTTPClient(api_base, api_key)
|
|
70
79
|
return Deployments(http, api_key=api_key, api_base=api_base, agents_ws_url=resolved_agents_ws_url)
|
|
71
80
|
|
|
@@ -125,10 +134,11 @@ def _resolve_agent(agent_id: str) -> str:
|
|
|
125
134
|
|
|
126
135
|
def _get_pod_with_token(agent_id: str) -> Agent:
|
|
127
136
|
"""Get an agent, filling JWT from local state if needed."""
|
|
137
|
+
resolved_agent_id = _resolve_agent(agent_id)
|
|
128
138
|
agents = _get_deployments_client()
|
|
129
|
-
pod = agents.get(
|
|
139
|
+
pod = agents.get(resolved_agent_id)
|
|
130
140
|
state = _load_state()
|
|
131
|
-
local = state.get(
|
|
141
|
+
local = state.get(resolved_agent_id, {})
|
|
132
142
|
if not pod.jwt_token and local.get("jwt_token"):
|
|
133
143
|
pod.jwt_token = local["jwt_token"]
|
|
134
144
|
if isinstance(pod, OpenClawAgent) and not pod.gateway_token and local.get("gateway_token"):
|
|
@@ -274,7 +284,7 @@ def create(
|
|
|
274
284
|
port: list[str] = typer.Option(None, "--port", help="Expose port as PORT or PORT:noauth. Repeatable."),
|
|
275
285
|
command: str = typer.Option(None, "--command", help="Container args as a shell-style string"),
|
|
276
286
|
entrypoint: str = typer.Option(None, "--entrypoint", help="Container entrypoint as a shell-style string"),
|
|
277
|
-
image: str = typer.Option(None, "--image", help="Override the default
|
|
287
|
+
image: str = typer.Option(None, "--image", help="Override the default OpenClaw image"),
|
|
278
288
|
registry_url: str = typer.Option(None, "--registry-url", help="Container registry URL for private image pulls"),
|
|
279
289
|
registry_username: str = typer.Option(None, "--registry-username", help="Registry username"),
|
|
280
290
|
registry_password: str = typer.Option(None, "--registry-password", help="Registry password"),
|
|
@@ -303,7 +313,7 @@ def create(
|
|
|
303
313
|
ports=ports_list,
|
|
304
314
|
command=command_argv,
|
|
305
315
|
entrypoint=entrypoint_argv,
|
|
306
|
-
image=image,
|
|
316
|
+
image=_default_openclaw_image(image),
|
|
307
317
|
registry_url=registry_url,
|
|
308
318
|
registry_auth=registry_auth,
|
|
309
319
|
gateway_token=gateway_token,
|
|
@@ -322,7 +332,7 @@ def create(
|
|
|
322
332
|
console.print(f" Size: {pod.cpu} CPU, {pod.memory} GB")
|
|
323
333
|
console.print(f" State: {pod.state}")
|
|
324
334
|
console.print(f" Desktop: {pod.vnc_url}")
|
|
325
|
-
console.print(f" Shell: {pod.shell_url}")
|
|
335
|
+
console.print(f" Shell: {'via hyper agents shell' if not pod.shell_url else pod.shell_url}")
|
|
326
336
|
display_ports = pod.ports or ports_list or []
|
|
327
337
|
for p in display_ports:
|
|
328
338
|
auth_text = "auth" if p.get("auth", True) else "noauth"
|
|
@@ -330,26 +340,14 @@ def create(
|
|
|
330
340
|
|
|
331
341
|
if wait and not pod.dry_run:
|
|
332
342
|
console.print("\n[dim]Waiting for pod to start...[/dim]")
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
elif pod.state in ("failed", "stopped"):
|
|
342
|
-
console.print(f"[red]❌ Agent failed: {pod.state}[/red]")
|
|
343
|
-
if pod.last_error:
|
|
344
|
-
console.print(f" Error: {pod.last_error}")
|
|
345
|
-
raise typer.Exit(1)
|
|
346
|
-
else:
|
|
347
|
-
console.print(f" [{i*5}s] State: {pod.state}")
|
|
348
|
-
except typer.Exit:
|
|
349
|
-
raise
|
|
350
|
-
except Exception as e:
|
|
351
|
-
console.print(f" [{i*5}s] Checking... ({e})")
|
|
352
|
-
else:
|
|
343
|
+
try:
|
|
344
|
+
pod = agents.wait_running(pod.id, timeout=300, poll_interval=5)
|
|
345
|
+
_save_pod_state(pod)
|
|
346
|
+
console.print(f"[green]✅ Agent is running![/green]")
|
|
347
|
+
except RuntimeError as e:
|
|
348
|
+
console.print(f"[red]❌ Agent failed: {e}[/red]")
|
|
349
|
+
raise typer.Exit(1)
|
|
350
|
+
except TimeoutError:
|
|
353
351
|
console.print("[yellow]⚠ Timed out (5 min). Pod may still be starting.[/yellow]")
|
|
354
352
|
|
|
355
353
|
if pod.dry_run:
|
|
@@ -360,6 +358,33 @@ def create(
|
|
|
360
358
|
console.print(f"Desktop: {pod.vnc_url}")
|
|
361
359
|
|
|
362
360
|
|
|
361
|
+
@app.command("wait")
|
|
362
|
+
def wait_agent(
|
|
363
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
364
|
+
timeout: int = typer.Option(300, "--timeout", help="Seconds to wait for RUNNING"),
|
|
365
|
+
poll_interval: float = typer.Option(5.0, "--poll-interval", help="Seconds between polls"),
|
|
366
|
+
):
|
|
367
|
+
"""Wait for an agent to reach RUNNING."""
|
|
368
|
+
agents = _get_deployments_client()
|
|
369
|
+
pod = _get_pod_with_token(agent_id)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
pod = agents.wait_running(pod.id, timeout=timeout, poll_interval=poll_interval)
|
|
373
|
+
except RuntimeError as e:
|
|
374
|
+
console.print(f"[red]❌ Agent failed: {e}[/red]")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
except TimeoutError as e:
|
|
377
|
+
console.print(f"[yellow]⚠ {e}[/yellow]")
|
|
378
|
+
raise typer.Exit(1)
|
|
379
|
+
|
|
380
|
+
_save_pod_state(pod)
|
|
381
|
+
console.print(f"[green]✅ Agent is running:[/green] [bold]{pod.id[:12]}[/bold]")
|
|
382
|
+
console.print(f" Name: {pod.name or pod.pod_name}")
|
|
383
|
+
console.print(f" State: {pod.state}")
|
|
384
|
+
console.print(f" Desktop: {pod.vnc_url}")
|
|
385
|
+
console.print(f" Shell: {'via hyper agents shell' if not pod.shell_url else pod.shell_url}")
|
|
386
|
+
|
|
387
|
+
|
|
363
388
|
@app.command("list")
|
|
364
389
|
def list_agents(
|
|
365
390
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
@@ -442,7 +467,7 @@ def status(
|
|
|
442
467
|
console.print(f" Size: {pod.cpu} CPU, {pod.memory} GB")
|
|
443
468
|
console.print(f" State: {pod.state}")
|
|
444
469
|
console.print(f" Desktop: {pod.vnc_url}")
|
|
445
|
-
console.print(f" Shell: {pod.shell_url}")
|
|
470
|
+
console.print(f" Shell: {'via hyper agents shell' if not pod.shell_url else pod.shell_url}")
|
|
446
471
|
console.print(f" Created: {pod.created_at}")
|
|
447
472
|
if pod.started_at:
|
|
448
473
|
console.print(f" Started: {pod.started_at}")
|
|
@@ -470,7 +495,7 @@ def start(
|
|
|
470
495
|
port: list[str] = typer.Option(None, "--port", help="Expose port as PORT or PORT:noauth. Repeatable."),
|
|
471
496
|
command: str = typer.Option(None, "--command", help="Container args as a shell-style string"),
|
|
472
497
|
entrypoint: str = typer.Option(None, "--entrypoint", help="Container entrypoint as a shell-style string"),
|
|
473
|
-
image: str = typer.Option(None, "--image", help="Override the default
|
|
498
|
+
image: str = typer.Option(None, "--image", help="Override the default OpenClaw image"),
|
|
474
499
|
registry_url: str = typer.Option(None, "--registry-url", help="Container registry URL for private image pulls"),
|
|
475
500
|
registry_username: str = typer.Option(None, "--registry-username", help="Registry username"),
|
|
476
501
|
registry_password: str = typer.Option(None, "--registry-password", help="Registry password"),
|
|
@@ -489,6 +514,7 @@ def start(
|
|
|
489
514
|
registry_auth = _build_registry_auth(registry_username, registry_password)
|
|
490
515
|
launch_config = dict(local.get("launch_config") or {})
|
|
491
516
|
effective_gateway_token = gateway_token or local.get("gateway_token")
|
|
517
|
+
effective_image = _default_openclaw_image(image, launch_config)
|
|
492
518
|
|
|
493
519
|
try:
|
|
494
520
|
pod = agents.start(
|
|
@@ -498,7 +524,7 @@ def start(
|
|
|
498
524
|
ports=ports_list,
|
|
499
525
|
command=command_argv,
|
|
500
526
|
entrypoint=entrypoint_argv,
|
|
501
|
-
image=
|
|
527
|
+
image=effective_image,
|
|
502
528
|
registry_url=registry_url,
|
|
503
529
|
registry_auth=registry_auth,
|
|
504
530
|
gateway_token=effective_gateway_token,
|
|
@@ -978,18 +1004,21 @@ def gateway_cron(
|
|
|
978
1004
|
def gateway_chat(
|
|
979
1005
|
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
980
1006
|
message: str = typer.Argument(..., help="Message to send"),
|
|
1007
|
+
session_key: str = typer.Option("main", "--session-key", help="Gateway chat session key"),
|
|
981
1008
|
):
|
|
982
1009
|
"""Send a chat message to an agent via the Gateway and stream the response."""
|
|
983
1010
|
pod = _require_openclaw_agent(_get_pod_with_token(agent_id))
|
|
984
1011
|
|
|
985
1012
|
async def _run():
|
|
986
|
-
async for event in pod.chat_send(message):
|
|
1013
|
+
async for event in pod.chat_send(message, session_key=session_key):
|
|
987
1014
|
if event.type == "content":
|
|
988
1015
|
print(event.text, end="", flush=True)
|
|
989
1016
|
elif event.type == "thinking":
|
|
990
1017
|
console.print(f"[dim]{event.text}[/dim]", end="")
|
|
991
1018
|
elif event.type == "tool_call":
|
|
992
1019
|
console.print(f"\n[yellow]🔧 {event.data}[/yellow]")
|
|
1020
|
+
elif event.type == "tool_result":
|
|
1021
|
+
console.print(f"\n[cyan]📤 {event.data}[/cyan]")
|
|
993
1022
|
elif event.type == "error":
|
|
994
1023
|
console.print(f"\n[red]❌ {event.text}[/red]")
|
|
995
1024
|
elif event.type == "done":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""HyperCLI - Main entry point"""
|
|
2
2
|
import sys
|
|
3
|
+
import json
|
|
3
4
|
import typer
|
|
4
5
|
from rich.console import Console
|
|
5
6
|
from rich.prompt import Prompt
|
|
@@ -143,7 +144,8 @@ def cli():
|
|
|
143
144
|
try:
|
|
144
145
|
app()
|
|
145
146
|
except APIError as e:
|
|
146
|
-
|
|
147
|
+
raw_detail = e.detail or str(e)
|
|
148
|
+
detail = raw_detail if isinstance(raw_detail, str) else json.dumps(raw_detail)
|
|
147
149
|
|
|
148
150
|
# Check for GPU type errors and suggest corrections
|
|
149
151
|
if "GPU type" in detail and "not found" in detail and "Available:" in detail:
|
|
@@ -17,10 +17,10 @@ DEV_API_BASE = "https://api.dev.hypercli.com"
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _get_api_key(key: str | None) -> str:
|
|
20
|
-
"""Resolve API key: --key flag > env
|
|
20
|
+
"""Resolve API key: --key flag > env HYPER_API_KEY > agent-key.json."""
|
|
21
21
|
if key:
|
|
22
22
|
return key
|
|
23
|
-
env_key = os.environ.get("
|
|
23
|
+
env_key = os.environ.get("HYPER_API_KEY", "").strip()
|
|
24
24
|
if env_key:
|
|
25
25
|
return env_key
|
|
26
26
|
if AGENT_KEY_PATH.exists():
|
|
@@ -29,7 +29,7 @@ def _get_api_key(key: str | None) -> str:
|
|
|
29
29
|
if k:
|
|
30
30
|
return k
|
|
31
31
|
console.print("[red]❌ No API key found.[/red]")
|
|
32
|
-
console.print("Pass [bold]--key sk-...[/bold], set [bold]
|
|
32
|
+
console.print("Pass [bold]--key sk-...[/bold], set [bold]HYPER_API_KEY[/bold], or run [bold]hyper agent subscribe[/bold]")
|
|
33
33
|
raise typer.Exit(1)
|
|
34
34
|
|
|
35
35
|
|
|
@@ -11,6 +11,21 @@ def get_client() -> HyperCLI:
|
|
|
11
11
|
return HyperCLI()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _parse_constraints(
|
|
15
|
+
constraints: Optional[list[str]],
|
|
16
|
+
cpu_vendor: Optional[str],
|
|
17
|
+
) -> dict[str, str] | None:
|
|
18
|
+
parsed: dict[str, str] = {}
|
|
19
|
+
for item in constraints or []:
|
|
20
|
+
key, sep, value = item.partition("=")
|
|
21
|
+
if not sep or not key or not value:
|
|
22
|
+
raise typer.BadParameter(f"Invalid constraint '{item}', expected KEY=VALUE")
|
|
23
|
+
parsed[key.strip()] = value.strip()
|
|
24
|
+
if cpu_vendor:
|
|
25
|
+
parsed.setdefault("cpu_vendor", cpu_vendor.strip())
|
|
26
|
+
return parsed or None
|
|
27
|
+
|
|
28
|
+
|
|
14
29
|
@app.command("list")
|
|
15
30
|
def list_instances(
|
|
16
31
|
gpu: Optional[str] = typer.Option(None, "--gpu", "-g", help="Filter by GPU type"),
|
|
@@ -64,7 +79,13 @@ def list_gpus(
|
|
|
64
79
|
|
|
65
80
|
if fmt == "json":
|
|
66
81
|
output({k: {"name": v.name, "description": v.description, "configs": [
|
|
67
|
-
{
|
|
82
|
+
{
|
|
83
|
+
"gpu_count": c.gpu_count,
|
|
84
|
+
"cpu_cores": c.cpu_cores,
|
|
85
|
+
"memory_gb": c.memory_gb,
|
|
86
|
+
"regions": c.regions,
|
|
87
|
+
"constraints": c.constraints,
|
|
88
|
+
}
|
|
68
89
|
for c in v.configs
|
|
69
90
|
]} for k, v in types.items()}, "json")
|
|
70
91
|
else:
|
|
@@ -187,6 +208,8 @@ def launch(
|
|
|
187
208
|
gpu: str = typer.Option("l40s", "--gpu", "-g", help="GPU type"),
|
|
188
209
|
count: int = typer.Option(1, "--count", "-n", help="Number of GPUs"),
|
|
189
210
|
region: Optional[str] = typer.Option(None, "--region", "-r", help="Region code"),
|
|
211
|
+
constraint: Optional[list[str]] = typer.Option(None, "--constraint", help="Placement constraint KEY=VALUE (repeatable)"),
|
|
212
|
+
cpu_vendor: Optional[str] = typer.Option(None, "--cpu-vendor", help="CPU vendor constraint (e.g. intel, amd)"),
|
|
190
213
|
runtime: Optional[int] = typer.Option(None, "--runtime", "-t", help="Runtime in seconds"),
|
|
191
214
|
interruptible: bool = typer.Option(True, "--interruptible/--on-demand", help="Use interruptible instances"),
|
|
192
215
|
env: Optional[list[str]] = typer.Option(None, "--env", "-e", help="Env vars (KEY=VALUE)"),
|
|
@@ -256,6 +279,8 @@ def launch(
|
|
|
256
279
|
if command and any(op in command for op in ["&&", "||", "|", ";", ">", "<", "$"]):
|
|
257
280
|
command = f'sh -c "{command}"'
|
|
258
281
|
|
|
282
|
+
constraints = _parse_constraints(constraint, cpu_vendor)
|
|
283
|
+
|
|
259
284
|
follow_api_key = None
|
|
260
285
|
|
|
261
286
|
if x402:
|
|
@@ -281,6 +306,7 @@ def launch(
|
|
|
281
306
|
gpu_type=gpu,
|
|
282
307
|
gpu_count=count,
|
|
283
308
|
region=region,
|
|
309
|
+
constraints=constraints,
|
|
284
310
|
interruptible=interruptible,
|
|
285
311
|
env=env_dict,
|
|
286
312
|
ports=ports_dict,
|
|
@@ -330,6 +356,7 @@ def launch(
|
|
|
330
356
|
gpu_type=gpu,
|
|
331
357
|
gpu_count=count,
|
|
332
358
|
region=region,
|
|
359
|
+
constraints=constraints,
|
|
333
360
|
runtime=runtime,
|
|
334
361
|
interruptible=interruptible,
|
|
335
362
|
env=env_dict,
|
|
@@ -18,10 +18,13 @@ DEFAULT_API_BASE = "https://api.hypercli.com"
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _get_api_key(key: str | None) -> str:
|
|
21
|
-
"""Resolve API key: --key
|
|
21
|
+
"""Resolve API key: --key > HYPER_API_KEY > HYPER_AGENTS_API_KEY > agent-key.json."""
|
|
22
22
|
if key:
|
|
23
23
|
return key
|
|
24
|
-
env_key = os.environ.get("
|
|
24
|
+
env_key = os.environ.get("HYPER_API_KEY", "").strip()
|
|
25
|
+
if env_key:
|
|
26
|
+
return env_key
|
|
27
|
+
env_key = os.environ.get("HYPER_AGENTS_API_KEY", "").strip()
|
|
25
28
|
if env_key:
|
|
26
29
|
return env_key
|
|
27
30
|
if AGENT_KEY_PATH.exists():
|
|
@@ -30,15 +33,18 @@ def _get_api_key(key: str | None) -> str:
|
|
|
30
33
|
if k:
|
|
31
34
|
return k
|
|
32
35
|
console.print("[red]❌ No API key found.[/red]")
|
|
33
|
-
console.print("Pass [bold]--key sk-...[/bold], set [bold]
|
|
36
|
+
console.print("Pass [bold]--key sk-...[/bold], set [bold]HYPER_API_KEY[/bold] or [bold]HYPER_AGENTS_API_KEY[/bold], or run [bold]hyper agent subscribe[/bold]")
|
|
34
37
|
raise typer.Exit(1)
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def _resolve_api_base(base_url: str | None) -> str:
|
|
38
|
-
"""Resolve API base: --base-url >
|
|
41
|
+
"""Resolve API base: --base-url > HYPER_API_BASE > HYPERCLI_API_URL > default."""
|
|
39
42
|
if base_url:
|
|
40
43
|
return base_url.rstrip("/")
|
|
41
|
-
env_base = os.environ.get("
|
|
44
|
+
env_base = os.environ.get("HYPER_API_BASE", "").strip()
|
|
45
|
+
if env_base:
|
|
46
|
+
return env_base.rstrip("/")
|
|
47
|
+
env_base = os.environ.get("HYPERCLI_API_URL", "").strip()
|
|
42
48
|
if env_base:
|
|
43
49
|
return env_base.rstrip("/")
|
|
44
50
|
return DEFAULT_API_BASE
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypercli-cli"
|
|
7
|
-
version = "2026.3.
|
|
7
|
+
version = "2026.3.22"
|
|
8
8
|
description = "CLI for HyperCLI - GPU orchestration and LLM API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ authors = [
|
|
|
13
13
|
{ name = "HyperCLI", email = "support@hypercli.com" }
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"hypercli-sdk>=2026.3.
|
|
16
|
+
"hypercli-sdk>=2026.3.22",
|
|
17
17
|
"typer>=0.20.0",
|
|
18
18
|
"rich>=14.2.0",
|
|
19
19
|
"websocket-client>=1.6.0",
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
comfyui = [
|
|
27
|
-
"hypercli-sdk[comfyui]>=2026.3.
|
|
27
|
+
"hypercli-sdk[comfyui]>=2026.3.22",
|
|
28
28
|
]
|
|
29
29
|
wallet = [
|
|
30
30
|
"x402[httpx,evm]>=2.0.0",
|
|
@@ -37,7 +37,7 @@ stt = [
|
|
|
37
37
|
"faster-whisper>=1.1.0",
|
|
38
38
|
]
|
|
39
39
|
all = [
|
|
40
|
-
"hypercli-sdk[comfyui]>=2026.3.
|
|
40
|
+
"hypercli-sdk[comfyui]>=2026.3.22",
|
|
41
41
|
"x402[httpx,evm]>=2.0.0",
|
|
42
42
|
"eth-account>=0.13.0",
|
|
43
43
|
"web3>=7.0.0",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_agents_cli_prefers_agent_key_env(monkeypatch):
|
|
5
|
+
monkeypatch.setenv("HYPER_API_KEY", "sk-product")
|
|
6
|
+
monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
|
|
7
|
+
|
|
8
|
+
import hypercli_cli.agents as agents
|
|
9
|
+
|
|
10
|
+
importlib.reload(agents)
|
|
11
|
+
|
|
12
|
+
assert agents._get_agent_api_key() == "sk-agent"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_agents_cli_prefers_agent_base_env(monkeypatch):
|
|
16
|
+
monkeypatch.setenv("AGENTS_API_BASE_URL", "https://api.agents.dev.hypercli.com")
|
|
17
|
+
monkeypatch.setenv("AGENTS_WS_URL", "wss://api.agents.dev.hypercli.com/ws")
|
|
18
|
+
monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
|
|
19
|
+
|
|
20
|
+
import hypercli_cli.agents as agents
|
|
21
|
+
|
|
22
|
+
importlib.reload(agents)
|
|
23
|
+
|
|
24
|
+
client = agents._get_deployments_client()
|
|
25
|
+
assert client._api_base == "https://api.dev.hypercli.com/agents"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_voice_cli_prefers_product_envs(monkeypatch):
|
|
29
|
+
monkeypatch.setenv("HYPER_API_KEY", "sk-product")
|
|
30
|
+
monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
|
|
31
|
+
monkeypatch.setenv("HYPER_API_BASE", "https://api.hypercli.com")
|
|
32
|
+
monkeypatch.setenv("HYPERCLI_API_URL", "https://api.dev.hypercli.com")
|
|
33
|
+
|
|
34
|
+
import hypercli_cli.voice as voice
|
|
35
|
+
|
|
36
|
+
importlib.reload(voice)
|
|
37
|
+
|
|
38
|
+
assert voice._get_api_key(None) == "sk-product"
|
|
39
|
+
assert voice._resolve_api_base(None) == "https://api.hypercli.com"
|
|
@@ -98,6 +98,52 @@ def test_instances_launch_dry_run_mock(monkeypatch):
|
|
|
98
98
|
assert "job-dryrun" in result.stdout
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def test_instances_launch_dry_run_includes_constraints(monkeypatch):
|
|
102
|
+
captured = {}
|
|
103
|
+
|
|
104
|
+
class FakeJobs:
|
|
105
|
+
def create(self, **kwargs):
|
|
106
|
+
captured.update(kwargs)
|
|
107
|
+
return SimpleNamespace(
|
|
108
|
+
job_id="job-dryrun",
|
|
109
|
+
state="validated",
|
|
110
|
+
gpu_type="h200",
|
|
111
|
+
gpu_count=8,
|
|
112
|
+
region="br",
|
|
113
|
+
constraints={"cpu_vendor": "amd"},
|
|
114
|
+
price_per_hour=12.34,
|
|
115
|
+
runtime=300,
|
|
116
|
+
cold_boot=False,
|
|
117
|
+
hostname=None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
121
|
+
monkeypatch.setattr("hypercli_cli.instances.get_client", lambda: fake_client)
|
|
122
|
+
|
|
123
|
+
result = runner.invoke(
|
|
124
|
+
app,
|
|
125
|
+
[
|
|
126
|
+
"instances",
|
|
127
|
+
"launch",
|
|
128
|
+
"nvidia/cuda:12.0-base-ubuntu22.04",
|
|
129
|
+
"--dry-run",
|
|
130
|
+
"--gpu",
|
|
131
|
+
"h200",
|
|
132
|
+
"--count",
|
|
133
|
+
"8",
|
|
134
|
+
"--cpu-vendor",
|
|
135
|
+
"amd",
|
|
136
|
+
"--constraint",
|
|
137
|
+
"stack=prod",
|
|
138
|
+
"--output",
|
|
139
|
+
"json",
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
assert result.exit_code == 0
|
|
144
|
+
assert captured["constraints"] == {"cpu_vendor": "amd", "stack": "prod"}
|
|
145
|
+
|
|
146
|
+
|
|
101
147
|
def test_agent_exec_command(monkeypatch):
|
|
102
148
|
called = {}
|
|
103
149
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from hypercli_cli.agent import _config_openclaw
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_config_openclaw_limits_runtime_models_to_supported_set():
|
|
5
|
+
api_key = "sk-test"
|
|
6
|
+
api_base = "https://api.agents.hypercli.com"
|
|
7
|
+
models = [
|
|
8
|
+
{"id": "kimi-k2.5", "name": "Kimi K2.5", "reasoning": True},
|
|
9
|
+
{"id": "glm-5", "name": "GLM-5", "reasoning": True},
|
|
10
|
+
{
|
|
11
|
+
"id": "qwen3-embedding-4b",
|
|
12
|
+
"name": "Qwen3 Embedding 4B",
|
|
13
|
+
"reasoning": False,
|
|
14
|
+
"mode": "embedding",
|
|
15
|
+
},
|
|
16
|
+
{"id": "claude-sonnet-4", "name": "Claude Sonnet 4", "reasoning": False},
|
|
17
|
+
{"id": "minimax-m2.5", "name": "MiniMax M2.5", "reasoning": False},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
config = _config_openclaw(api_key, models, api_base)
|
|
21
|
+
providers = config["models"]["providers"]
|
|
22
|
+
|
|
23
|
+
assert set(providers) == {"hyperclaw", "kimi-coding"}
|
|
24
|
+
assert [m["id"] for m in providers["hyperclaw"]["models"]] == ["glm-5"]
|
|
25
|
+
assert [m["id"] for m in providers["kimi-coding"]["models"]] == ["kimi-k2.5"]
|
|
26
|
+
|
|
27
|
+
defaults = config["agents"]["defaults"]
|
|
28
|
+
assert defaults["model"]["primary"] == "kimi-coding/kimi-k2.5"
|
|
29
|
+
assert defaults["memorySearch"]["provider"] == "openai"
|
|
30
|
+
assert defaults["memorySearch"]["model"] == "qwen3-embedding-4b"
|
|
31
|
+
assert defaults["memorySearch"]["remote"]["baseUrl"] == "https://api.agents.hypercli.com/v1"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_config_openclaw_uses_first_embedding_model_for_memory_search():
|
|
35
|
+
config = _config_openclaw(
|
|
36
|
+
"sk-test",
|
|
37
|
+
[
|
|
38
|
+
{"id": "kimi-k2.5", "name": "Kimi K2.5", "reasoning": True},
|
|
39
|
+
{"id": "text-embedding-3-large", "name": "Text Embedding 3 Large", "mode": "embedding"},
|
|
40
|
+
],
|
|
41
|
+
"https://api.agents.hypercli.com",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
defaults = config["agents"]["defaults"]
|
|
45
|
+
assert defaults["memorySearch"]["model"] == "text-embedding-3-large"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_config_openclaw_supports_placeholder_api_key_env():
|
|
49
|
+
config = _config_openclaw(
|
|
50
|
+
"sk-real",
|
|
51
|
+
[
|
|
52
|
+
{"id": "kimi-k2.5", "name": "Kimi K2.5", "reasoning": True},
|
|
53
|
+
{"id": "glm-5", "name": "GLM-5", "reasoning": True},
|
|
54
|
+
{"id": "qwen3-embedding-4b", "name": "Qwen3 Embedding 4B", "mode": "embedding"},
|
|
55
|
+
],
|
|
56
|
+
"https://api.agents.hypercli.com",
|
|
57
|
+
placeholder_env="HYPER_API_KEY",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
providers = config["models"]["providers"]
|
|
61
|
+
assert providers["hyperclaw"]["apiKey"] == "${HYPER_API_KEY}"
|
|
62
|
+
assert providers["kimi-coding"]["apiKey"] == "${HYPER_API_KEY}"
|
|
63
|
+
assert config["agents"]["defaults"]["memorySearch"]["remote"]["apiKey"] == "${HYPER_API_KEY}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2026.3.13"
|
|
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
|