hypercli-sdk 2026.4.13.post4__tar.gz → 2026.4.18__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.18}/PKG-INFO +4 -1
  2. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/README.md +3 -0
  3. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/__init__.py +1 -1
  4. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/agent.py +33 -109
  5. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/agents.py +18 -1
  6. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/voice.py +21 -7
  7. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/pyproject.toml +1 -1
  8. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_agents.py +40 -24
  9. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_agents.py +51 -1
  10. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_claw.py +2 -256
  11. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_voice.py +11 -0
  12. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/.gitignore +0 -0
  13. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/billing.py +0 -0
  14. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/client.py +0 -0
  15. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/config.py +0 -0
  16. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/files.py +0 -0
  17. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/gateway.py +0 -0
  18. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/http.py +0 -0
  19. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/instances.py +0 -0
  20. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/__init__.py +0 -0
  21. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/base.py +0 -0
  22. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/comfyui.py +0 -0
  23. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/gradio.py +0 -0
  24. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/jobs.py +0 -0
  25. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/keys.py +0 -0
  26. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/logs.py +0 -0
  27. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/models.py +0 -0
  28. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/openclaw/__init__.py +0 -0
  29. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/openclaw/gateway.py +0 -0
  30. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/renders.py +0 -0
  31. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/shell.py +0 -0
  32. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/user.py +0 -0
  33. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/x402.py +0 -0
  34. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/conftest.py +0 -0
  35. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_auth.py +0 -0
  36. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_billing.py +0 -0
  37. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_instances.py +0 -0
  38. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_jobs_dryrun.py +0 -0
  39. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_keys.py +0 -0
  40. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_renders.py +0 -0
  41. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_apply_params.py +0 -0
  42. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_console_test_key.py +0 -0
  43. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_dev_test_keys.py +0 -0
  44. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_config.py +0 -0
  45. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_exec_shell_dryrun.py +0 -0
  46. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_gateway.py +0 -0
  47. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_gateway_retry.py +0 -0
  48. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_graph_to_api.py +0 -0
  49. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_http.py +0 -0
  50. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_jobs.py +0 -0
  51. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_keys.py +0 -0
  52. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_models.py +0 -0
  53. {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/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.18
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.18"
82
82
  __all__ = [
83
83
  "HyperCLI",
84
84
  "configure",
@@ -221,6 +221,36 @@ class HyperAgentEntitlements:
221
221
  )
222
222
 
223
223
 
224
+ @dataclass
225
+ class HyperAgentEntitlements:
226
+ """Effective account entitlements computed by the backend."""
227
+
228
+ effective_plan_id: str
229
+ pooled_tpm_limit: int
230
+ pooled_rpm_limit: int
231
+ pooled_tpd: int
232
+ slot_inventory: dict[str, Any]
233
+ active_entitlement_count: int
234
+
235
+ @classmethod
236
+ def from_dict(cls, data: dict) -> "HyperAgentEntitlements":
237
+ payload = data.get("entitlements") if isinstance(data.get("entitlements"), dict) else data
238
+ return cls(
239
+ effective_plan_id=payload.get("effective_plan_id", data.get("effective_plan_id", "")),
240
+ pooled_tpm_limit=int(payload.get("pooled_tpm_limit", data.get("pooled_tpm_limit", 0)) or 0),
241
+ pooled_rpm_limit=int(payload.get("pooled_rpm_limit", data.get("pooled_rpm_limit", 0)) or 0),
242
+ pooled_tpd=int(payload.get("pooled_tpd", data.get("pooled_tpd", 0)) or 0),
243
+ slot_inventory=payload.get("slot_inventory") or data.get("slot_inventory") or {},
244
+ active_entitlement_count=int(
245
+ payload.get(
246
+ "active_entitlement_count",
247
+ data.get("active_entitlement_count", data.get("active_subscription_count", 0)),
248
+ )
249
+ or 0
250
+ ),
251
+ )
252
+
253
+
224
254
  @dataclass
225
255
  class HyperAgentSubscriptionSummary:
226
256
  """Effective entitlement summary for an authenticated HyperClaw user."""
@@ -236,7 +266,6 @@ class HyperAgentSubscriptionSummary:
236
266
  active_subscription_count: int
237
267
  active_entitlement_count: int
238
268
  entitlements: HyperAgentEntitlements
239
- entitlement_items: list[HyperAgentEntitlement]
240
269
  active_subscriptions: list[HyperAgentSubscription]
241
270
  subscriptions: list[HyperAgentSubscription]
242
271
  user: dict[str, Any]
@@ -257,7 +286,6 @@ class HyperAgentSubscriptionSummary:
257
286
  active_subscription_count=int(data.get("active_subscription_count", 0) or 0),
258
287
  active_entitlement_count=int(data.get("active_entitlement_count", data.get("active_subscription_count", 0)) or 0),
259
288
  entitlements=HyperAgentEntitlements.from_dict(data),
260
- entitlement_items=[HyperAgentEntitlement.from_dict(item) for item in data.get("entitlement_items", [])],
261
289
  active_subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("active_subscriptions", [])],
262
290
  subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("subscriptions", [])],
263
291
  user=data.get("user") or {},
@@ -267,21 +295,6 @@ class HyperAgentSubscriptionSummary:
267
295
  HyperAgentEntitlementsSummary = HyperAgentSubscriptionSummary
268
296
 
269
297
 
270
- @dataclass
271
- class HyperAgentSubscriptionMutationResult:
272
- ok: bool
273
- message: str
274
- subscription: HyperAgentSubscription | None = None
275
-
276
- @classmethod
277
- def from_dict(cls, data: dict) -> "HyperAgentSubscriptionMutationResult":
278
- return cls(
279
- ok=bool(data.get("ok", False)),
280
- message=str(data.get("message") or ""),
281
- subscription=HyperAgentSubscription.from_dict(data["subscription"]) if data.get("subscription") else None,
282
- )
283
-
284
-
285
298
  @dataclass
286
299
  class HyperAgentModel:
287
300
  """Available model on HyperAgent."""
@@ -888,102 +901,13 @@ class HyperAgent:
888
901
  response.raise_for_status()
889
902
  return HyperAgentEntitlementsSummary.from_dict(response.json())
890
903
 
891
- def entitlement_instances(self) -> list[HyperAgentEntitlement]:
892
- response = self._http._session.get(
893
- f"{self._control_base_url}/entitlements/instances",
894
- headers={"Authorization": f"Bearer {self._api_key}"},
895
- )
896
- response.raise_for_status()
897
- data = response.json()
898
- return [HyperAgentEntitlement.from_dict(item) for item in data.get("items", [])]
899
-
900
- def update_subscription(self, subscription_id: str, bundle: dict[str, int] | None) -> HyperAgentSubscriptionMutationResult:
904
+ def cancel_subscription(self, subscription_id: str) -> Dict[str, Any]:
901
905
  response = self._http._session.post(
902
- f"{self._control_base_url}/subscriptions/{subscription_id}/update",
906
+ f"{self._control_base_url}/subscriptions/{subscription_id}/cancel",
903
907
  headers={"Authorization": f"Bearer {self._api_key}"},
904
- json={"bundle": dict(bundle or {})},
905
908
  )
906
909
  response.raise_for_status()
907
- return HyperAgentSubscriptionMutationResult.from_dict(response.json())
908
-
909
- def cancel_subscription(self, subscription_id: str) -> HyperAgentSubscriptionMutationResult:
910
- return self.update_subscription(subscription_id, {})
911
-
912
- def usage_summary(self) -> HyperAgentUsageSummary:
913
- return HyperAgentUsageSummary.from_dict(self._control_get("/usage"))
914
-
915
- def usage_history(self, days: int = 7) -> HyperAgentUsageHistory:
916
- return HyperAgentUsageHistory.from_dict(self._control_get("/usage/history", params={"days": days}))
917
-
918
- def key_usage(self, days: int = 7) -> HyperAgentKeyUsage:
919
- return HyperAgentKeyUsage.from_dict(self._control_get("/usage/keys", params={"days": days}))
920
-
921
- def agent_types(self) -> HyperAgentTypeCatalog:
922
- return HyperAgentTypeCatalog.from_dict(self._control_get("/types"))
923
-
924
- def billing_info(self) -> HyperAgentBillingInfo:
925
- return HyperAgentBillingInfo.from_dict(self._control_get("/billing/info").get("company_billing", {}))
926
-
927
- def billing_profile(self) -> HyperAgentBillingProfileResponse:
928
- return HyperAgentBillingProfileResponse.from_dict(self._control_get("/billing/profile"))
929
-
930
- def update_billing_profile(self, profile: HyperAgentBillingProfileFields) -> HyperAgentBillingProfileResponse:
931
- return HyperAgentBillingProfileResponse.from_dict(
932
- self._control_put("/billing/profile", payload=profile.to_dict())
933
- )
934
-
935
- def payments(
936
- self,
937
- *,
938
- limit: int | None = None,
939
- provider: str | None = None,
940
- status: str | None = None,
941
- ) -> HyperAgentPaymentsResponse:
942
- params: dict[str, Any] = {}
943
- if limit is not None:
944
- params["limit"] = limit
945
- if provider:
946
- params["provider"] = provider
947
- if status:
948
- params["status"] = status
949
- return HyperAgentPaymentsResponse.from_dict(self._control_get("/billing/payments", params=params or None))
950
-
951
- def payment(self, payment_id: str) -> HyperAgentPayment:
952
- return HyperAgentPayment.from_dict(self._control_get(f"/billing/payments/{payment_id}"))
953
-
954
- def create_stripe_checkout(
955
- self,
956
- *,
957
- bundle: dict[str, int] | None = None,
958
- quantity: int | None = None,
959
- success_url: str | None = None,
960
- cancel_url: str | None = None,
961
- plan_id: str | None = None,
962
- ) -> HyperAgentStripeCheckoutResponse:
963
- payload: dict[str, Any] = {}
964
- if bundle is not None:
965
- payload["bundle"] = dict(bundle)
966
- if quantity is not None:
967
- payload["quantity"] = quantity
968
- if success_url is not None:
969
- payload["success_url"] = success_url
970
- if cancel_url is not None:
971
- payload["cancel_url"] = cancel_url
972
- path = f"/stripe/{plan_id}" if plan_id else "/stripe/checkout"
973
- return HyperAgentStripeCheckoutResponse.from_dict(self._control_post(path, payload=payload))
974
-
975
- def create_x402_checkout(
976
- self,
977
- *,
978
- bundle: dict[str, int] | None = None,
979
- quantity: int | None = None,
980
- ) -> HyperAgentX402CheckoutResponse:
981
- payload: dict[str, Any] = {}
982
- if bundle is not None:
983
- payload["bundle"] = dict(bundle)
984
- if quantity is not None:
985
- payload["quantity"] = quantity
986
- return HyperAgentX402CheckoutResponse.from_dict(self._control_post("/x402/checkout", payload=payload))
910
+ return response.json()
987
911
 
988
912
  def discovery_health(self) -> Dict[str, Any]:
989
913
  response = self._http._session.get(f"{self._api_base_without_v1()}/discovery/health")
@@ -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.18"
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
@@ -7,7 +7,6 @@ from unittest.mock import Mock, patch, MagicMock
7
7
  from hypercli import HyperCLI
8
8
  from hypercli.agent import (
9
9
  HyperAgent,
10
- HyperAgentEntitlement,
11
10
  HyperAgentEntitlements,
12
11
  HyperAgentEntitlementsSummary,
13
12
  HyperAgentPlan,
@@ -97,27 +96,9 @@ class TestHyperAgentDataclasses:
97
96
  "pooled_tpm_limit": 2000,
98
97
  "pooled_rpm_limit": 20,
99
98
  "pooled_tpd": 2000000,
100
- "billing_reset_at": "2026-04-15T00:00:00Z",
101
99
  "slot_inventory": {"large": {"granted": 2, "used": 1, "available": 1}},
102
100
  "active_entitlement_count": 1,
103
101
  },
104
- "entitlement_items": [
105
- {
106
- "id": "ent-1",
107
- "user_id": "user-1",
108
- "subscription_id": "sub-1",
109
- "plan_id": "large",
110
- "plan_name": "Large",
111
- "provider": "STRIPE",
112
- "status": "ACTIVE",
113
- "expires_at": "2026-04-15T00:00:00Z",
114
- "agent_tier": "large",
115
- "features": {"voice": True},
116
- "tags": ["customer=acme"],
117
- "active_agent_count": 1,
118
- "active_agent_ids": ["agent-1"],
119
- }
120
- ],
121
102
  "active_subscriptions": [
122
103
  {
123
104
  "id": "sub-1",
@@ -137,8 +118,6 @@ class TestHyperAgentDataclasses:
137
118
  assert summary.active_subscription_count == 1
138
119
  assert isinstance(summary.entitlements, HyperAgentEntitlements)
139
120
  assert summary.entitlements.active_entitlement_count == 1
140
- assert summary.billing_reset_at is not None
141
- assert summary.entitlements.billing_reset_at is not None
142
121
  assert summary.active_subscriptions[0].plan_id == "large"
143
122
  assert isinstance(summary.entitlement_items[0], HyperAgentEntitlement)
144
123
  assert summary.entitlement_items[0].tags == ["customer=acme"]
@@ -279,7 +258,6 @@ class TestHyperAgentClient:
279
258
  "pooled_tpm_limit": 2000,
280
259
  "pooled_rpm_limit": 20,
281
260
  "pooled_tpd": 2000000,
282
- "billing_reset_at": "2026-04-15T00:00:00Z",
283
261
  "slot_inventory": {"large": {"granted": 2, "used": 1, "available": 1}},
284
262
  "active_subscription_count": 1,
285
263
  "active_entitlement_count": 1,
@@ -288,26 +266,9 @@ class TestHyperAgentClient:
288
266
  "pooled_tpm_limit": 2000,
289
267
  "pooled_rpm_limit": 20,
290
268
  "pooled_tpd": 2000000,
291
- "billing_reset_at": "2026-04-15T00:00:00Z",
292
269
  "slot_inventory": {"large": {"granted": 2, "used": 1, "available": 1}},
293
270
  "active_entitlement_count": 1,
294
271
  },
295
- "entitlement_items": [
296
- {
297
- "id": "ent-1",
298
- "user_id": "user-1",
299
- "plan_id": "large",
300
- "plan_name": "Large",
301
- "provider": "X402",
302
- "status": "ACTIVE",
303
- "expires_at": "2026-04-20T00:00:00Z",
304
- "agent_tier": "large",
305
- "features": {"voice": True},
306
- "tags": ["customer=acme"],
307
- "active_agent_count": 0,
308
- "active_agent_ids": [],
309
- }
310
- ],
311
272
  "active_subscriptions": [],
312
273
  "subscriptions": [],
313
274
  "user": {"id": "user-1", "team_id": "team-1"},
@@ -318,242 +279,27 @@ class TestHyperAgentClient:
318
279
  summary = agent.entitlements()
319
280
 
320
281
  assert isinstance(summary, HyperAgentEntitlementsSummary)
321
- assert summary.billing_reset_at is not None
322
282
  assert summary.entitlements.slot_inventory["large"]["available"] == 1
323
- assert summary.entitlement_items[0].provider == "X402"
324
283
  mock_http._session.get.assert_called_with(
325
284
  "https://api.hypercli.com/agents/entitlements",
326
285
  headers={"Authorization": "Bearer sk-hyper-test"},
327
286
  )
328
287
 
329
- def test_entitlement_instances(self, mock_http):
330
- mock_http._session.get.return_value.json.return_value = {
331
- "items": [
332
- {
333
- "id": "ent-1",
334
- "user_id": "user-1",
335
- "subscription_id": None,
336
- "plan_id": "large",
337
- "plan_name": "Large",
338
- "provider": "X402",
339
- "status": "ACTIVE",
340
- "expires_at": "2026-04-20T00:00:00Z",
341
- "agent_tier": "large",
342
- "features": {"voice": True},
343
- "tags": ["customer=acme"],
344
- "active_agent_count": 0,
345
- "active_agent_ids": [],
346
- }
347
- ]
348
- }
349
- mock_http._session.get.return_value.raise_for_status = Mock()
350
-
351
- agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
352
- entitlements = agent.entitlement_instances()
353
-
354
- assert len(entitlements) == 1
355
- assert entitlements[0].plan_id == "large"
356
- assert entitlements[0].tags == ["customer=acme"]
357
- mock_http._session.get.assert_called_with(
358
- "https://api.hypercli.com/agents/entitlements/instances",
359
- headers={"Authorization": "Bearer sk-hyper-test"},
360
- )
361
-
362
288
  def test_cancel_subscription(self, mock_http):
363
289
  mock_http._session.post.return_value.json.return_value = {
364
290
  "ok": True,
365
291
  "message": "Subscription will be cancelled at the end of the current billing period",
366
- "subscription": {
367
- "id": "sub-1",
368
- "user_id": "user-1",
369
- "plan_id": "large",
370
- "plan_name": "Large",
371
- "provider": "STRIPE",
372
- "status": "ACTIVE",
373
- "cancel_at_period_end": True,
374
- "can_cancel": True,
375
- },
376
292
  }
377
293
  mock_http._session.post.return_value.raise_for_status = Mock()
378
294
 
379
295
  agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
380
296
  result = agent.cancel_subscription("sub-1")
381
297
 
382
- assert result.ok is True
383
- assert result.subscription is not None
384
- assert result.subscription.cancel_at_period_end is True
385
- mock_http._session.post.assert_called_with(
386
- "https://api.hypercli.com/agents/subscriptions/sub-1/update",
387
- headers={"Authorization": "Bearer sk-hyper-test"},
388
- json={"bundle": {}},
389
- )
390
-
391
- def test_update_subscription(self, mock_http):
392
- mock_http._session.post.return_value.json.return_value = {
393
- "ok": True,
394
- "message": "Subscription upgraded immediately",
395
- "subscription": {
396
- "id": "sub-1",
397
- "user_id": "user-1",
398
- "plan_id": "large",
399
- "plan_name": "Large",
400
- "provider": "STRIPE",
401
- "status": "ACTIVE",
402
- "cancel_at_period_end": False,
403
- "can_cancel": True,
404
- },
405
- }
406
- mock_http._session.post.return_value.raise_for_status = Mock()
407
-
408
- agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
409
- result = agent.update_subscription("sub-1", {"large": 1})
410
-
411
- assert result.ok is True
412
- assert result.subscription is not None
413
- assert result.subscription.plan_id == "large"
298
+ assert result["ok"] is True
414
299
  mock_http._session.post.assert_called_with(
415
- "https://api.hypercli.com/agents/subscriptions/sub-1/update",
300
+ "https://api.hypercli.com/agents/subscriptions/sub-1/cancel",
416
301
  headers={"Authorization": "Bearer sk-hyper-test"},
417
- json={"bundle": {"large": 1}},
418
302
  )
419
-
420
- def test_usage_endpoints(self, mock_http):
421
- mock_http._session.get.side_effect = [
422
- Mock(json=Mock(return_value={
423
- "total_tokens": 100,
424
- "prompt_tokens": 60,
425
- "completion_tokens": 40,
426
- "request_count": 5,
427
- "active_keys": 2,
428
- "current_tpm": 1000,
429
- "current_rpm": 10,
430
- "period": "30d",
431
- }), raise_for_status=Mock()),
432
- Mock(json=Mock(return_value={
433
- "history": [{"date": "2026-04-13", "total_tokens": 100, "prompt_tokens": 60, "completion_tokens": 40, "requests": 5}],
434
- "days": 7,
435
- }), raise_for_status=Mock()),
436
- Mock(json=Mock(return_value={
437
- "keys": [{"key_hash": "key-1", "name": "Primary", "total_tokens": 100, "prompt_tokens": 60, "completion_tokens": 40, "requests": 5}],
438
- "days": 7,
439
- }), raise_for_status=Mock()),
440
- ]
441
-
442
- agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
443
- summary = agent.usage_summary()
444
- history = agent.usage_history()
445
- keys = agent.key_usage()
446
-
447
- assert isinstance(summary, HyperAgentUsageSummary)
448
- assert summary.total_tokens == 100
449
- assert isinstance(history, HyperAgentUsageHistory)
450
- assert history.history[0].date == "2026-04-13"
451
- assert isinstance(keys, HyperAgentKeyUsage)
452
- assert keys.keys[0].key_hash == "key-1"
453
-
454
- def test_types_and_billing_endpoints(self, mock_http):
455
- mock_http._session.get.side_effect = [
456
- Mock(json=Mock(return_value={
457
- "types": [{"id": "medium", "name": "Medium", "cpu": 1, "memory": 2, "cpu_limit": 1, "memory_limit": 2}],
458
- "plans": [{"id": "2aiu", "name": "2 AIU", "price": 20, "agents": 1, "agent_type": "medium", "highlighted": True}],
459
- }), raise_for_status=Mock()),
460
- Mock(json=Mock(return_value={
461
- "company_billing": {"address": ["HyperCLI"], "email": "support@hypercli.com"},
462
- "profile": None,
463
- }), raise_for_status=Mock()),
464
- Mock(json=Mock(return_value={
465
- "company_billing": {"address": ["HyperCLI"], "email": "support@hypercli.com"},
466
- "profile": {"billing_name": "Test User"},
467
- }), raise_for_status=Mock()),
468
- ]
469
- mock_http._session.put.return_value.json.return_value = {
470
- "company_billing": {"address": ["HyperCLI"], "email": "support@hypercli.com"},
471
- "profile": {"billing_name": "Test User"},
472
- "synced_stripe_customer_ids": ["cus_123"],
473
- }
474
- mock_http._session.put.return_value.raise_for_status = Mock()
475
-
476
- agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
477
- catalog = agent.agent_types()
478
- info = agent.billing_info()
479
- profile = agent.billing_profile()
480
- updated = agent.update_billing_profile(HyperAgentBillingProfileFields(billing_name="Test User"))
481
-
482
- assert isinstance(catalog, HyperAgentTypeCatalog)
483
- assert catalog.types[0].id == "medium"
484
- assert isinstance(info, HyperAgentBillingInfo)
485
- assert info.email == "support@hypercli.com"
486
- assert isinstance(profile, HyperAgentBillingProfileResponse)
487
- assert profile.profile is not None
488
- assert profile.profile.billing_name == "Test User"
489
- assert updated.synced_stripe_customer_ids == ["cus_123"]
490
-
491
- def test_payments_and_checkout_endpoints(self, mock_http):
492
- mock_http._session.get.side_effect = [
493
- Mock(json=Mock(return_value={
494
- "items": [{
495
- "id": "pay_123",
496
- "user_id": "user-1",
497
- "subscription_id": None,
498
- "entitlement_id": None,
499
- "provider": "STRIPE",
500
- "status": "SUCCEEDED",
501
- "amount": "2000",
502
- "currency": "usd",
503
- "external_payment_id": "pi_123",
504
- "created_at": "2026-04-13T00:00:00Z",
505
- "updated_at": "2026-04-13T00:00:00Z",
506
- "user": {"id": "user-1", "email": "user@example.com", "wallet_address": None, "team_id": "team-1", "plan_id": "2aiu"},
507
- "subscription": None,
508
- "entitlement": None,
509
- }]
510
- }), raise_for_status=Mock()),
511
- Mock(json=Mock(return_value={
512
- "id": "pay_123",
513
- "user_id": "user-1",
514
- "subscription_id": None,
515
- "entitlement_id": None,
516
- "provider": "STRIPE",
517
- "status": "SUCCEEDED",
518
- "amount": "2000",
519
- "currency": "usd",
520
- "external_payment_id": "pi_123",
521
- "created_at": "2026-04-13T00:00:00Z",
522
- "updated_at": "2026-04-13T00:00:00Z",
523
- "user": {"id": "user-1", "email": "user@example.com", "wallet_address": None, "team_id": "team-1", "plan_id": "2aiu"},
524
- "subscription": None,
525
- "entitlement": None,
526
- }), raise_for_status=Mock()),
527
- ]
528
- mock_http._session.post.side_effect = [
529
- Mock(json=Mock(return_value={"checkout_url": "https://checkout.stripe.test/session"}), raise_for_status=Mock()),
530
- Mock(json=Mock(return_value={
531
- "ok": True,
532
- "key": "sk-x402",
533
- "plan_id": "2aiu",
534
- "quantity": 1,
535
- "bundle": {"medium": 1},
536
- "amount_paid": "20.000000",
537
- "duration_days": 30,
538
- "expires_at": "2026-05-13T00:00:00Z",
539
- "tpm_limit": 1000,
540
- "rpm_limit": 10,
541
- }), raise_for_status=Mock()),
542
- ]
543
-
544
- agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
545
- payments = agent.payments(limit=10, provider="stripe", status="succeeded")
546
- payment = agent.payment("pay_123")
547
- stripe = agent.create_stripe_checkout(bundle={"medium": 1})
548
- x402 = agent.create_x402_checkout(bundle={"medium": 1})
549
-
550
- assert isinstance(payments, HyperAgentPaymentsResponse)
551
- assert payments.items[0].id == "pay_123"
552
- assert payment.external_payment_id == "pi_123"
553
- assert isinstance(stripe, HyperAgentStripeCheckoutResponse)
554
- assert stripe.checkout_url == "https://checkout.stripe.test/session"
555
- assert isinstance(x402, HyperAgentX402CheckoutResponse)
556
- assert x402.plan_id == "2aiu"
557
303
 
558
304
  def test_openai_client_creation(self, mock_http):
559
305
  """Test that OpenAI client is created with correct config."""
@@ -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