hypercli-sdk 2026.4.13.post4__tar.gz → 2026.4.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/PKG-INFO +4 -1
  2. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/README.md +3 -0
  3. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/__init__.py +1 -1
  4. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/agents.py +18 -1
  5. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/voice.py +21 -7
  6. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/pyproject.toml +1 -1
  7. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_agents.py +40 -24
  8. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_agents.py +51 -1
  9. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_voice.py +11 -0
  10. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/.gitignore +0 -0
  11. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/agent.py +0 -0
  12. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/billing.py +0 -0
  13. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/client.py +0 -0
  14. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/config.py +0 -0
  15. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/files.py +0 -0
  16. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/gateway.py +0 -0
  17. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/http.py +0 -0
  18. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/instances.py +0 -0
  19. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/job/__init__.py +0 -0
  20. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/job/base.py +0 -0
  21. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/job/comfyui.py +0 -0
  22. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/job/gradio.py +0 -0
  23. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/jobs.py +0 -0
  24. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/keys.py +0 -0
  25. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/logs.py +0 -0
  26. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/models.py +0 -0
  27. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/openclaw/__init__.py +0 -0
  28. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/openclaw/gateway.py +0 -0
  29. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/renders.py +0 -0
  30. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/shell.py +0 -0
  31. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/user.py +0 -0
  32. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/hypercli/x402.py +0 -0
  33. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/conftest.py +0 -0
  34. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_auth.py +0 -0
  35. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_billing.py +0 -0
  36. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_instances.py +0 -0
  37. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_jobs_dryrun.py +0 -0
  38. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_keys.py +0 -0
  39. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/integration/test_renders.py +0 -0
  40. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_apply_params.py +0 -0
  41. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_bootstrap_console_test_key.py +0 -0
  42. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_bootstrap_dev_test_keys.py +0 -0
  43. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_claw.py +0 -0
  44. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_config.py +0 -0
  45. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_exec_shell_dryrun.py +0 -0
  46. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_gateway.py +0 -0
  47. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_gateway_retry.py +0 -0
  48. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_graph_to_api.py +0 -0
  49. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_http.py +0 -0
  50. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_jobs.py +0 -0
  51. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_keys.py +0 -0
  52. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_models.py +0 -0
  53. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.17}/tests/test_renders_subscription.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 2026.4.13.post4
3
+ Version: 2026.4.17
4
4
  Summary: Python SDK for HyperCLI - GPU orchestration and HyperAgent API
5
5
  Project-URL: Homepage, https://hypercli.com
6
6
  Project-URL: Documentation, https://docs.hypercli.com
@@ -140,11 +140,14 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
140
140
  agent = client.deployments.create_openclaw(
141
141
  name="docs-demo",
142
142
  start=True,
143
+ heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
143
144
  registry_url="git.nedos.co",
144
145
  registry_auth={"username": "ci", "password": "token"},
145
146
  )
146
147
  ```
147
148
 
149
+ `heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
150
+
148
151
  ## Error Handling
149
152
 
150
153
  ```python
@@ -109,11 +109,14 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
109
109
  agent = client.deployments.create_openclaw(
110
110
  name="docs-demo",
111
111
  start=True,
112
+ heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
112
113
  registry_url="git.nedos.co",
113
114
  registry_auth={"username": "ci", "password": "token"},
114
115
  )
115
116
  ```
116
117
 
118
+ `heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
119
+
117
120
  ## Error Handling
118
121
 
119
122
  ```python
@@ -78,7 +78,7 @@ from .gateway import (
78
78
  extract_gateway_chat_tool_calls,
79
79
  normalize_gateway_chat_message,
80
80
  )
81
- __version__ = "2026.4.13.post4"
81
+ __version__ = "2026.4.17"
82
82
  __all__ = [
83
83
  "HyperCLI",
84
84
  "configure",
@@ -191,14 +191,23 @@ def _build_agent_launch(
191
191
  registry_url: str | None = None,
192
192
  registry_auth: dict | None = None,
193
193
  gateway_token: str | None = None,
194
+ heartbeat: dict | None = None,
194
195
  ) -> tuple[dict, str]:
195
- prepared_config = dict(config or {})
196
+ prepared_config = copy.deepcopy(config or {})
196
197
  nested_launch_keys = sorted(LAUNCH_CONFIG_KEYS.intersection(prepared_config.keys()))
197
198
  if nested_launch_keys:
198
199
  raise ValueError(
199
200
  "Launch settings must be top-level fields, not nested under config: "
200
201
  + ", ".join(nested_launch_keys)
201
202
  )
203
+ if heartbeat:
204
+ agents_cfg = dict(prepared_config.get("agents") or {})
205
+ defaults_cfg = dict(agents_cfg.get("defaults") or {})
206
+ heartbeat_cfg = dict(defaults_cfg.get("heartbeat") or {})
207
+ heartbeat_cfg.update(dict(heartbeat))
208
+ defaults_cfg["heartbeat"] = heartbeat_cfg
209
+ agents_cfg["defaults"] = defaults_cfg
210
+ prepared_config["agents"] = agents_cfg
202
211
  env_map = dict(env or {})
203
212
  if env:
204
213
  env_map.update(env)
@@ -1015,6 +1024,7 @@ class Deployments:
1015
1024
  registry_url: str = None,
1016
1025
  registry_auth: dict = None,
1017
1026
  gateway_token: str = None,
1027
+ heartbeat: dict = None,
1018
1028
  meta_ui: dict = None,
1019
1029
  dry_run: bool = False,
1020
1030
  start: bool = True,
@@ -1045,6 +1055,7 @@ class Deployments:
1045
1055
  registry_url=registry_url,
1046
1056
  registry_auth=registry_auth,
1047
1057
  gateway_token=gateway_token,
1058
+ heartbeat=heartbeat,
1048
1059
  )
1049
1060
  body: dict = {**launch_payload, "start": start}
1050
1061
  if dry_run:
@@ -1083,6 +1094,7 @@ class Deployments:
1083
1094
  registry_url: str = None,
1084
1095
  registry_auth: dict = None,
1085
1096
  gateway_token: str = None,
1097
+ heartbeat: dict = None,
1086
1098
  meta_ui: dict = None,
1087
1099
  dry_run: bool = False,
1088
1100
  start: bool = True,
@@ -1110,6 +1122,7 @@ class Deployments:
1110
1122
  registry_url=registry_url,
1111
1123
  registry_auth=registry_auth,
1112
1124
  gateway_token=gateway_token,
1125
+ heartbeat=heartbeat,
1113
1126
  meta_ui=meta_ui,
1114
1127
  dry_run=dry_run,
1115
1128
  start=start,
@@ -1185,6 +1198,7 @@ class Deployments:
1185
1198
  registry_url: str = None,
1186
1199
  registry_auth: dict = None,
1187
1200
  gateway_token: str = None,
1201
+ heartbeat: dict = None,
1188
1202
  dry_run: bool = False,
1189
1203
  ) -> Agent:
1190
1204
  """Start a previously stopped agent.
@@ -1208,6 +1222,7 @@ class Deployments:
1208
1222
  registry_url=registry_url,
1209
1223
  registry_auth=registry_auth,
1210
1224
  gateway_token=gateway_token,
1225
+ heartbeat=heartbeat,
1211
1226
  )
1212
1227
  body: dict[str, Any] = dict(launch_payload)
1213
1228
  if dry_run:
@@ -1236,6 +1251,7 @@ class Deployments:
1236
1251
  registry_url: str = None,
1237
1252
  registry_auth: dict = None,
1238
1253
  gateway_token: str = None,
1254
+ heartbeat: dict = None,
1239
1255
  dry_run: bool = False,
1240
1256
  openclaw_routes: dict | None = None,
1241
1257
  openclaw_route_options: dict | None = None,
@@ -1259,6 +1275,7 @@ class Deployments:
1259
1275
  registry_url=registry_url,
1260
1276
  registry_auth=registry_auth,
1261
1277
  gateway_token=gateway_token,
1278
+ heartbeat=heartbeat,
1262
1279
  dry_run=dry_run,
1263
1280
  )
1264
1281
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  """Voice API client."""
4
+ import os
4
5
  from pathlib import Path
5
6
  from typing import TYPE_CHECKING
6
7
  import base64
@@ -17,9 +18,22 @@ def _encode_reference_audio(ref_audio: bytes | str | Path) -> str:
17
18
  return base64.b64encode(audio_bytes).decode()
18
19
 
19
20
 
21
+ def _default_voice_timeout() -> float:
22
+ raw_value = os.environ.get("HYPER_VOICE_TIMEOUT_SECONDS", "300").strip()
23
+ try:
24
+ return float(raw_value)
25
+ except ValueError:
26
+ return 300.0
27
+
28
+
29
+ def _resolve_voice_timeout(timeout: float | None) -> float:
30
+ if timeout is not None:
31
+ return float(timeout)
32
+ return _default_voice_timeout()
33
+
34
+
20
35
  class VoiceAPI:
21
36
  """Voice capability API wrapper."""
22
-
23
37
  DEFAULT_TIMEOUT = 300.0
24
38
 
25
39
  def __init__(self, http: "HTTPClient"):
@@ -32,7 +46,7 @@ class VoiceAPI:
32
46
  voice: str = "Chelsie",
33
47
  language: str = "auto",
34
48
  response_format: str = "mp3",
35
- timeout: float = DEFAULT_TIMEOUT,
49
+ timeout: float | None = None,
36
50
  ) -> bytes:
37
51
  return self._http.post_bytes(
38
52
  "/agents/voice/tts",
@@ -42,7 +56,7 @@ class VoiceAPI:
42
56
  "language": language,
43
57
  "response_format": response_format,
44
58
  },
45
- timeout=timeout,
59
+ timeout=_resolve_voice_timeout(timeout),
46
60
  )
47
61
 
48
62
  def clone(
@@ -53,7 +67,7 @@ class VoiceAPI:
53
67
  language: str = "auto",
54
68
  x_vector_only: bool = True,
55
69
  response_format: str = "mp3",
56
- timeout: float = DEFAULT_TIMEOUT,
70
+ timeout: float | None = None,
57
71
  ) -> bytes:
58
72
  return self._http.post_bytes(
59
73
  "/agents/voice/clone",
@@ -64,7 +78,7 @@ class VoiceAPI:
64
78
  "x_vector_only": x_vector_only,
65
79
  "response_format": response_format,
66
80
  },
67
- timeout=timeout,
81
+ timeout=_resolve_voice_timeout(timeout),
68
82
  )
69
83
 
70
84
  def design(
@@ -74,7 +88,7 @@ class VoiceAPI:
74
88
  description: str,
75
89
  language: str = "auto",
76
90
  response_format: str = "mp3",
77
- timeout: float = DEFAULT_TIMEOUT,
91
+ timeout: float | None = None,
78
92
  ) -> bytes:
79
93
  return self._http.post_bytes(
80
94
  "/agents/voice/design",
@@ -84,5 +98,5 @@ class VoiceAPI:
84
98
  "language": language,
85
99
  "response_format": response_format,
86
100
  },
87
- timeout=timeout,
101
+ timeout=_resolve_voice_timeout(timeout),
88
102
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-sdk"
7
- version = "2026.4.13.post4"
7
+ version = "2026.4.17"
8
8
  description = "Python SDK for HyperCLI - GPU orchestration and HyperAgent API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -12,32 +12,48 @@ def _create_agent_with_available_tier(client: HyperCLI, name: str, tags: list[st
12
12
  budget = client.deployments.budget()
13
13
  slots = (budget or {}).get("slots") or {}
14
14
 
15
- tier = next((candidate for candidate in ("large", "medium", "small") if int((slots.get(candidate) or {}).get("available") or 0) > 0), None)
16
- if not tier:
15
+ tiers = [
16
+ candidate
17
+ for candidate in ("large", "medium", "small")
18
+ if int((slots.get(candidate) or {}).get("available") or 0) > 0
19
+ ]
20
+ if not tiers:
17
21
  raise AssertionError("No available entitlement slots for integration agent tests")
18
22
 
19
- agent_id: str | None = None
20
- try:
21
- agent = client.deployments.create(
22
- name=name,
23
- size=tier,
24
- start=False,
25
- tags=tags,
26
- )
27
- agent_id = agent.id
28
- client.deployments.start_openclaw(agent.id, dry_run=True)
29
- return agent.id, tier
30
- except APIError as exc:
31
- if agent_id:
32
- try:
33
- client.deployments.delete(agent_id)
34
- except APIError:
35
- pass
36
- if exc.status_code == 429:
37
- raise AssertionError(
38
- f"Budget reported '{tier}' available but dry-run start was rejected for slot exhaustion"
39
- ) from exc
40
- raise
23
+ last_error: Exception | None = None
24
+ for tier in tiers:
25
+ agent_id: str | None = None
26
+ try:
27
+ agent = client.deployments.create(
28
+ name=name,
29
+ size=tier,
30
+ start=False,
31
+ tags=tags,
32
+ )
33
+ agent_id = agent.id
34
+ client.deployments.start_openclaw(agent.id, dry_run=True)
35
+ return agent.id, tier
36
+ except APIError as exc:
37
+ if agent_id:
38
+ try:
39
+ client.deployments.delete(agent_id)
40
+ except APIError:
41
+ pass
42
+ if exc.status_code == 429:
43
+ last_error = AssertionError(
44
+ f"Budget reported '{tier}' available but dry-run start was rejected for slot exhaustion"
45
+ )
46
+ continue
47
+ if exc.status_code == 503 and "No connected clusters available for tags" in str(exc.detail):
48
+ last_error = AssertionError(
49
+ f"Tier '{tier}' has entitlement capacity but no connected clusters are advertising that tag"
50
+ )
51
+ continue
52
+ raise
53
+
54
+ if last_error:
55
+ raise last_error
56
+ raise AssertionError("Failed to create an integration agent with any available tier")
41
57
 
42
58
 
43
59
  def test_list_agents_requires_agent_key(client, test_agent_api_key: str):
@@ -416,6 +416,27 @@ def test_build_agent_launch_includes_command_and_entrypoint():
416
416
  assert launch["routes"] == {"web": {"port": 80, "prefix": ""}}
417
417
 
418
418
 
419
+ def test_build_agent_launch_merges_heartbeat_defaults():
420
+ launch, _gateway_token = _build_agent_launch(
421
+ {"agents": {"defaults": {"model": "openai/gpt-5.4", "heartbeat": {"target": "last"}}}},
422
+ heartbeat={"every": "0m", "includeSystemPromptSection": False},
423
+ gateway_token="gw-token",
424
+ )
425
+
426
+ assert launch["config"] == {
427
+ "agents": {
428
+ "defaults": {
429
+ "model": "openai/gpt-5.4",
430
+ "heartbeat": {
431
+ "target": "last",
432
+ "every": "0m",
433
+ "includeSystemPromptSection": False,
434
+ },
435
+ }
436
+ }
437
+ }
438
+
439
+
419
440
  def test_build_openclaw_routes_defaults():
420
441
  assert build_openclaw_routes() == {
421
442
  "openclaw": {"port": 18789, "auth": False, "prefix": ""},
@@ -485,6 +506,35 @@ def test_create_openclaw_respects_explicit_empty_routes(agents_client):
485
506
  assert posted_json["image"] == DEFAULT_OPENCLAW_IMAGE
486
507
  assert posted_json["routes"] == {}
487
508
 
509
+
510
+ def test_create_openclaw_includes_heartbeat_when_requested(agents_client):
511
+ with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
512
+ mock_client = MagicMock()
513
+ mock_response = Mock()
514
+ mock_response.status_code = 200
515
+ mock_response.json.return_value = {
516
+ "id": "agent-123",
517
+ "user_id": "user-456",
518
+ "pod_id": "pod-789",
519
+ "pod_name": "test-pod",
520
+ "state": "starting",
521
+ }
522
+ mock_client.post.return_value = mock_response
523
+ mock_client.__enter__.return_value = mock_client
524
+ mock_client.__exit__.return_value = False
525
+ mock_client_class.return_value = mock_client
526
+
527
+ agents_client.create_openclaw(
528
+ name="test-agent",
529
+ heartbeat={"every": "0m", "includeSystemPromptSection": False},
530
+ )
531
+
532
+ posted_json = mock_client.post.call_args[1]["json"]
533
+ assert posted_json["config"]["agents"]["defaults"]["heartbeat"] == {
534
+ "every": "0m",
535
+ "includeSystemPromptSection": False,
536
+ }
537
+
488
538
  @pytest.fixture
489
539
  def mock_http():
490
540
  http = Mock(spec=HTTPClient)
@@ -970,7 +1020,7 @@ def test_agents_create_scoped_key(agents_client):
970
1020
  "key_id": "key-123",
971
1021
  "name": "agent-client",
972
1022
  "api_key": "hyper_api_scoped",
973
- "tags": ["agent=agent-123"],
1023
+ "tags": ["agent:agent-123"],
974
1024
  }
975
1025
  mock_client.post.return_value = mock_response
976
1026
  mock_client.__enter__.return_value = mock_client
@@ -70,3 +70,14 @@ def test_voice_design_posts_description():
70
70
  VoiceAPI.DEFAULT_TIMEOUT,
71
71
  )
72
72
  ]
73
+
74
+
75
+ def test_voice_timeout_uses_env_default(monkeypatch):
76
+ monkeypatch.setenv("HYPER_VOICE_TIMEOUT_SECONDS", "720")
77
+
78
+ http = DummyVoiceHTTP()
79
+ voice = VoiceAPI(http)
80
+
81
+ voice.tts("hello")
82
+
83
+ assert http.calls[0][2] == 720.0