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.
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/PKG-INFO +4 -1
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/README.md +3 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/__init__.py +1 -1
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/agent.py +33 -109
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/agents.py +18 -1
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/voice.py +21 -7
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_agents.py +40 -24
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_agents.py +51 -1
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_claw.py +2 -256
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_voice.py +11 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/.gitignore +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/client.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/gateway.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/models.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/openclaw/__init__.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/openclaw/gateway.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/renders.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_console_test_key.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_dev_test_keys.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_gateway.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_keys.py +0 -0
- {hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_models.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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}/
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
)
|
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/integration/test_jobs_dryrun.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_console_test_key.py
RENAMED
|
File without changes
|
{hypercli_sdk-2026.4.13.post4 → hypercli_sdk-2026.4.18}/tests/test_bootstrap_dev_test_keys.py
RENAMED
|
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
|