hypercli-sdk 2026.4.7__tar.gz → 2026.4.13__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 (54) hide show
  1. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/PKG-INFO +14 -1
  2. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/README.md +13 -0
  3. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/__init__.py +8 -1
  4. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/agent.py +148 -2
  5. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/agents.py +56 -15
  6. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/client.py +2 -0
  7. hypercli_sdk-2026.4.13/hypercli/gateway.py +7 -0
  8. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/http.py +2 -2
  9. hypercli_sdk-2026.4.13/hypercli/models.py +33 -0
  10. hypercli_sdk-2026.4.13/hypercli/openclaw/__init__.py +25 -0
  11. {hypercli_sdk-2026.4.7/hypercli → hypercli_sdk-2026.4.13/hypercli/openclaw}/gateway.py +7 -2
  12. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/renders.py +67 -4
  13. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/voice.py +8 -0
  14. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/pyproject.toml +1 -1
  15. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_agents.py +29 -31
  16. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_keys.py +13 -2
  17. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_agents.py +77 -4
  18. hypercli_sdk-2026.4.13/tests/test_bootstrap_console_test_key.py +81 -0
  19. hypercli_sdk-2026.4.13/tests/test_bootstrap_dev_test_keys.py +104 -0
  20. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_claw.py +206 -0
  21. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_gateway.py +52 -2
  22. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_gateway_retry.py +4 -2
  23. hypercli_sdk-2026.4.13/tests/test_models.py +27 -0
  24. hypercli_sdk-2026.4.13/tests/test_renders_subscription.py +188 -0
  25. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_voice.py +6 -3
  26. hypercli_sdk-2026.4.7/tests/test_renders_subscription.py +0 -86
  27. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/.gitignore +0 -0
  28. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/billing.py +0 -0
  29. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/config.py +0 -0
  30. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/files.py +0 -0
  31. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/instances.py +0 -0
  32. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/job/__init__.py +0 -0
  33. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/job/base.py +0 -0
  34. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/job/comfyui.py +0 -0
  35. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/job/gradio.py +0 -0
  36. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/jobs.py +0 -0
  37. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/keys.py +0 -0
  38. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/logs.py +0 -0
  39. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/shell.py +0 -0
  40. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/user.py +0 -0
  41. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/hypercli/x402.py +0 -0
  42. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/conftest.py +0 -0
  43. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_auth.py +0 -0
  44. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_billing.py +0 -0
  45. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_instances.py +0 -0
  46. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_jobs_dryrun.py +0 -0
  47. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/integration/test_renders.py +0 -0
  48. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_apply_params.py +0 -0
  49. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_config.py +0 -0
  50. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_exec_shell_dryrun.py +0 -0
  51. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_graph_to_api.py +0 -0
  52. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_http.py +0 -0
  53. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_jobs.py +0 -0
  54. {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.13}/tests/test_keys.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 2026.4.7
3
+ Version: 2026.4.13
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
@@ -132,6 +132,19 @@ response = client.chat.completions.create(
132
132
  )
133
133
  ```
134
134
 
135
+ ## OpenClaw Agents
136
+
137
+ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_auth`, `sync_root`, and `sync_enabled` are generic deployment options; the OpenClaw helpers only add defaults such as routes, image, and `sync_root=/home/node`.
138
+
139
+ ```python
140
+ agent = client.deployments.create_openclaw(
141
+ name="docs-demo",
142
+ start=True,
143
+ registry_url="git.nedos.co",
144
+ registry_auth={"username": "ci", "password": "token"},
145
+ )
146
+ ```
147
+
135
148
  ## Error Handling
136
149
 
137
150
  ```python
@@ -101,6 +101,19 @@ response = client.chat.completions.create(
101
101
  )
102
102
  ```
103
103
 
104
+ ## OpenClaw Agents
105
+
106
+ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_auth`, `sync_root`, and `sync_enabled` are generic deployment options; the OpenClaw helpers only add defaults such as routes, image, and `sync_root=/home/node`.
107
+
108
+ ```python
109
+ agent = client.deployments.create_openclaw(
110
+ name="docs-demo",
111
+ start=True,
112
+ registry_url="git.nedos.co",
113
+ registry_auth={"username": "ci", "password": "token"},
114
+ )
115
+ ```
116
+
104
117
  ## Error Handling
105
118
 
106
119
  ```python
@@ -32,6 +32,7 @@ from .jobs import (
32
32
  )
33
33
  from .renders import Render, RenderStatus
34
34
  from .voice import VoiceAPI
35
+ from .models import Model, ModelsAPI
35
36
  from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, FlowCatalogItem
36
37
  from .files import File, AsyncFiles
37
38
  from .job import BaseJob, ComfyUIJob, GradioJob, apply_params, apply_graph_modes, find_node, find_nodes, load_template, graph_to_api, expand_subgraphs, DEFAULT_OBJECT_INFO
@@ -42,6 +43,8 @@ from .agent import (
42
43
  HyperAgent,
43
44
  HyperAgentPlan,
44
45
  HyperAgentCurrentPlan,
46
+ HyperAgentEntitlements,
47
+ HyperAgentEntitlementsSummary,
45
48
  HyperAgentSubscription,
46
49
  HyperAgentSubscriptionSummary,
47
50
  HyperAgentModel,
@@ -57,7 +60,7 @@ from .gateway import (
57
60
  extract_gateway_chat_tool_calls,
58
61
  normalize_gateway_chat_message,
59
62
  )
60
- __version__ = "2026.4.7"
63
+ __version__ = "2026.4.13"
61
64
  __all__ = [
62
65
  "HyperCLI",
63
66
  "configure",
@@ -93,6 +96,8 @@ __all__ = [
93
96
  "Render",
94
97
  "RenderStatus",
95
98
  "VoiceAPI",
99
+ "Model",
100
+ "ModelsAPI",
96
101
  # x402 API
97
102
  "X402Client",
98
103
  "X402JobLaunch",
@@ -138,6 +143,8 @@ __all__ = [
138
143
  "HyperAgent",
139
144
  "HyperAgentPlan",
140
145
  "HyperAgentCurrentPlan",
146
+ "HyperAgentEntitlements",
147
+ "HyperAgentEntitlementsSummary",
141
148
  "HyperAgentSubscription",
142
149
  "HyperAgentSubscriptionSummary",
143
150
  "HyperAgentModel",
@@ -85,7 +85,7 @@ class HyperAgentCurrentPlan:
85
85
 
86
86
  @dataclass
87
87
  class HyperAgentSubscription:
88
- """A purchased HyperClaw entitlement/subscription instance."""
88
+ """A recurring HyperClaw billing subscription."""
89
89
 
90
90
  id: str
91
91
  user_id: str
@@ -106,10 +106,11 @@ class HyperAgentSubscription:
106
106
  plan_tpd: int = 0
107
107
  plan_agent_tier: str | None = None
108
108
  slot_grants: dict[str, int] | None = None
109
+ entitlements: list["HyperAgentEntitlement"] | None = None
109
110
 
110
111
  @classmethod
111
112
  def from_dict(cls, data: dict) -> "HyperAgentSubscription":
112
- expires_at = data.get("expires_at")
113
+ expires_at = data.get("current_period_end", data.get("expires_at"))
113
114
  updated_at = data.get("updated_at")
114
115
  return cls(
115
116
  id=data["id"],
@@ -131,6 +132,92 @@ class HyperAgentSubscription:
131
132
  plan_tpd=int(data.get("plan_tpd", 0) or 0),
132
133
  plan_agent_tier=data.get("plan_agent_tier"),
133
134
  slot_grants=data.get("slot_grants") or None,
135
+ entitlements=[HyperAgentEntitlement.from_dict(item) for item in data.get("entitlements", [])] or None,
136
+ )
137
+
138
+
139
+ @dataclass
140
+ class HyperAgentEntitlement:
141
+ """A concrete 1:1 entitlement grant."""
142
+
143
+ id: str
144
+ user_id: str
145
+ subscription_id: str | None
146
+ plan_id: str
147
+ plan_name: str
148
+ provider: str
149
+ status: str
150
+ expires_at: datetime | None = None
151
+ updated_at: datetime | None = None
152
+ tpm_limit: int = 0
153
+ rpm_limit: int = 0
154
+ tpd_limit: int = 0
155
+ agent_tier: str | None = None
156
+ features: dict[str, bool] | None = None
157
+ tags: list[str] | None = None
158
+ meta: dict[str, Any] | None = None
159
+ slot_grants: dict[str, int] | None = None
160
+ active_agent_count: int = 0
161
+ active_agent_ids: list[str] | None = None
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: dict) -> "HyperAgentEntitlement":
165
+ expires_at = data.get("expires_at")
166
+ updated_at = data.get("updated_at")
167
+ return cls(
168
+ id=data["id"],
169
+ user_id=data.get("user_id", ""),
170
+ subscription_id=data.get("subscription_id"),
171
+ plan_id=data.get("plan_id", ""),
172
+ plan_name=data.get("plan_name", data.get("plan_id", "")),
173
+ provider=data.get("provider", ""),
174
+ status=data.get("status", ""),
175
+ expires_at=datetime.fromisoformat(str(expires_at).replace("Z", "+00:00")) if expires_at else None,
176
+ updated_at=datetime.fromisoformat(str(updated_at).replace("Z", "+00:00")) if updated_at else None,
177
+ tpm_limit=int(data.get("tpm_limit", 0) or 0),
178
+ rpm_limit=int(data.get("rpm_limit", 0) or 0),
179
+ tpd_limit=int(data.get("tpd_limit", 0) or 0),
180
+ agent_tier=data.get("agent_tier"),
181
+ features=data.get("features") or {},
182
+ tags=data.get("tags") or [],
183
+ meta=data.get("meta") or None,
184
+ slot_grants=data.get("slot_grants") or None,
185
+ active_agent_count=int(data.get("active_agent_count", 0) or 0),
186
+ active_agent_ids=data.get("active_agent_ids") or [],
187
+ )
188
+
189
+
190
+ @dataclass
191
+ class HyperAgentEntitlements:
192
+ """Effective account entitlements computed by the backend."""
193
+
194
+ effective_plan_id: str
195
+ pooled_tpm_limit: int
196
+ pooled_rpm_limit: int
197
+ pooled_tpd: int
198
+ slot_inventory: dict[str, Any]
199
+ active_entitlement_count: int
200
+ billing_reset_at: datetime | None = None
201
+
202
+ @classmethod
203
+ def from_dict(cls, data: dict) -> "HyperAgentEntitlements":
204
+ payload = data.get("entitlements") if isinstance(data.get("entitlements"), dict) else data
205
+ return cls(
206
+ effective_plan_id=payload.get("effective_plan_id", data.get("effective_plan_id", "")),
207
+ pooled_tpm_limit=int(payload.get("pooled_tpm_limit", data.get("pooled_tpm_limit", 0)) or 0),
208
+ pooled_rpm_limit=int(payload.get("pooled_rpm_limit", data.get("pooled_rpm_limit", 0)) or 0),
209
+ pooled_tpd=int(payload.get("pooled_tpd", data.get("pooled_tpd", 0)) or 0),
210
+ slot_inventory=payload.get("slot_inventory") or data.get("slot_inventory") or {},
211
+ active_entitlement_count=int(
212
+ payload.get(
213
+ "active_entitlement_count",
214
+ data.get("active_entitlement_count", data.get("active_subscription_count", 0)),
215
+ )
216
+ or 0
217
+ ),
218
+ billing_reset_at=datetime.fromisoformat(str(payload.get("billing_reset_at")).replace("Z", "+00:00"))
219
+ if payload.get("billing_reset_at")
220
+ else None,
134
221
  )
135
222
 
136
223
 
@@ -140,11 +227,16 @@ class HyperAgentSubscriptionSummary:
140
227
 
141
228
  effective_plan_id: str
142
229
  current_subscription_id: str | None
230
+ current_entitlement_id: str | None
143
231
  pooled_tpm_limit: int
144
232
  pooled_rpm_limit: int
145
233
  pooled_tpd: int
146
234
  slot_inventory: dict[str, Any]
235
+ billing_reset_at: datetime | None
147
236
  active_subscription_count: int
237
+ active_entitlement_count: int
238
+ entitlements: HyperAgentEntitlements
239
+ entitlement_items: list[HyperAgentEntitlement]
148
240
  active_subscriptions: list[HyperAgentSubscription]
149
241
  subscriptions: list[HyperAgentSubscription]
150
242
  user: dict[str, Any]
@@ -154,17 +246,42 @@ class HyperAgentSubscriptionSummary:
154
246
  return cls(
155
247
  effective_plan_id=data.get("effective_plan_id", ""),
156
248
  current_subscription_id=data.get("current_subscription_id"),
249
+ current_entitlement_id=data.get("current_entitlement_id", data.get("current_subscription_id")),
157
250
  pooled_tpm_limit=int(data.get("pooled_tpm_limit", 0) or 0),
158
251
  pooled_rpm_limit=int(data.get("pooled_rpm_limit", 0) or 0),
159
252
  pooled_tpd=int(data.get("pooled_tpd", 0) or 0),
160
253
  slot_inventory=data.get("slot_inventory") or {},
254
+ billing_reset_at=datetime.fromisoformat(str(data.get("billing_reset_at")).replace("Z", "+00:00"))
255
+ if data.get("billing_reset_at")
256
+ else None,
161
257
  active_subscription_count=int(data.get("active_subscription_count", 0) or 0),
258
+ active_entitlement_count=int(data.get("active_entitlement_count", data.get("active_subscription_count", 0)) or 0),
259
+ entitlements=HyperAgentEntitlements.from_dict(data),
260
+ entitlement_items=[HyperAgentEntitlement.from_dict(item) for item in data.get("entitlement_items", [])],
162
261
  active_subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("active_subscriptions", [])],
163
262
  subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("subscriptions", [])],
164
263
  user=data.get("user") or {},
165
264
  )
166
265
 
167
266
 
267
+ HyperAgentEntitlementsSummary = HyperAgentSubscriptionSummary
268
+
269
+
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
+
168
285
  @dataclass
169
286
  class HyperAgentModel:
170
287
  """Available model on HyperAgent."""
@@ -368,6 +485,35 @@ class HyperAgent:
368
485
  response.raise_for_status()
369
486
  return HyperAgentSubscriptionSummary.from_dict(response.json())
370
487
 
488
+ def entitlements(self) -> HyperAgentEntitlementsSummary:
489
+ response = self._http._session.get(
490
+ f"{self._control_base_url}/entitlements",
491
+ headers={"Authorization": f"Bearer {self._api_key}"},
492
+ )
493
+ response.raise_for_status()
494
+ return HyperAgentEntitlementsSummary.from_dict(response.json())
495
+
496
+ def entitlement_instances(self) -> list[HyperAgentEntitlement]:
497
+ response = self._http._session.get(
498
+ f"{self._control_base_url}/entitlements/instances",
499
+ headers={"Authorization": f"Bearer {self._api_key}"},
500
+ )
501
+ response.raise_for_status()
502
+ data = response.json()
503
+ return [HyperAgentEntitlement.from_dict(item) for item in data.get("items", [])]
504
+
505
+ def update_subscription(self, subscription_id: str, bundle: dict[str, int] | None) -> HyperAgentSubscriptionMutationResult:
506
+ response = self._http._session.post(
507
+ f"{self._control_base_url}/subscriptions/{subscription_id}/update",
508
+ headers={"Authorization": f"Bearer {self._api_key}"},
509
+ json={"bundle": dict(bundle or {})},
510
+ )
511
+ response.raise_for_status()
512
+ return HyperAgentSubscriptionMutationResult.from_dict(response.json())
513
+
514
+ def cancel_subscription(self, subscription_id: str) -> HyperAgentSubscriptionMutationResult:
515
+ return self.update_subscription(subscription_id, {})
516
+
371
517
  def discovery_health(self) -> Dict[str, Any]:
372
518
  response = self._http._session.get(f"{self._api_base_without_v1()}/discovery/health")
373
519
  response.raise_for_status()
@@ -32,7 +32,7 @@ DEV_AGENTS_API_BASE = "https://api.dev.hypercli.com/agents"
32
32
  DEV_AGENTS_WS_URL = "wss://api.agents.dev.hypercli.com/ws"
33
33
  DEFAULT_OPENCLAW_IMAGE = "ghcr.io/hypercli/hypercli-openclaw:prod"
34
34
  LAUNCH_CONFIG_KEYS = frozenset({"image", "env", "routes", "ports", "command", "entrypoint", "sync_root", "sync_enabled", "registry_url", "registry_auth"})
35
- DEFAULT_OPENCLAW_SYNC_ROOT = "/home/ubuntu"
35
+ DEFAULT_OPENCLAW_SYNC_ROOT = "/home/node"
36
36
 
37
37
 
38
38
  def _is_directory_listing_payload(value: object) -> bool:
@@ -386,6 +386,28 @@ class Agent:
386
386
  self._deployments = agent._deployments
387
387
  return self
388
388
 
389
+ def update(
390
+ self,
391
+ *,
392
+ name: str | None = None,
393
+ size: str | None = None,
394
+ refresh_from_lagoon: bool | None = None,
395
+ last_error: str | None = None,
396
+ ) -> "Agent":
397
+ agent = self._require_deployments().update(
398
+ self.id,
399
+ name=name,
400
+ size=size,
401
+ refresh_from_lagoon=refresh_from_lagoon,
402
+ last_error=last_error,
403
+ )
404
+ self.__dict__.update(agent.__dict__)
405
+ self._deployments = agent._deployments
406
+ return self
407
+
408
+ def resize(self, *, size: str | None = None) -> "Agent":
409
+ return self.update(size=size)
410
+
389
411
  def env(self) -> dict[str, str]:
390
412
  """Fetch runtime environment from the pod's K8s secret."""
391
413
  data = self._require_deployments().env(self.id)
@@ -980,8 +1002,6 @@ class Deployments:
980
1002
  self,
981
1003
  name: str = None,
982
1004
  size: str = None,
983
- cpu: int = None,
984
- memory: int = None,
985
1005
  config: dict = None,
986
1006
  tags: list[str] = None,
987
1007
  env: dict = None,
@@ -1004,8 +1024,6 @@ class Deployments:
1004
1024
  Args:
1005
1025
  name: Agent name.
1006
1026
  size: Size preset (small/medium/large). Default: medium.
1007
- cpu: Custom CPU in cores (overrides size).
1008
- memory: Custom memory in GB (overrides size).
1009
1027
  config: Optional config overrides.
1010
1028
  env: Optional environment variables to pass through to the pod.
1011
1029
  ports: Optional exposed ports config.
@@ -1035,10 +1053,6 @@ class Deployments:
1035
1053
  body["name"] = name
1036
1054
  if size:
1037
1055
  body["size"] = size
1038
- if cpu is not None:
1039
- body["cpu"] = cpu
1040
- if memory is not None:
1041
- body["memory"] = memory
1042
1056
  if meta_ui:
1043
1057
  body["meta"] = {"ui": copy.deepcopy(meta_ui)}
1044
1058
  if tags:
@@ -1056,8 +1070,6 @@ class Deployments:
1056
1070
  self,
1057
1071
  name: str = None,
1058
1072
  size: str = None,
1059
- cpu: int = None,
1060
- memory: int = None,
1061
1073
  config: dict = None,
1062
1074
  tags: list[str] = None,
1063
1075
  env: dict = None,
@@ -1077,14 +1089,13 @@ class Deployments:
1077
1089
  openclaw_routes: dict | None = None,
1078
1090
  openclaw_route_options: dict | None = None,
1079
1091
  ) -> Agent:
1092
+ effective_env = dict(env or {})
1080
1093
  return self.create(
1081
1094
  name=name,
1082
1095
  size=size,
1083
- cpu=cpu,
1084
- memory=memory,
1085
1096
  config=config,
1086
1097
  tags=tags,
1087
- env=env,
1098
+ env=effective_env,
1088
1099
  ports=ports,
1089
1100
  routes=_resolve_openclaw_routes(
1090
1101
  routes,
@@ -1229,10 +1240,11 @@ class Deployments:
1229
1240
  openclaw_routes: dict | None = None,
1230
1241
  openclaw_route_options: dict | None = None,
1231
1242
  ) -> Agent:
1243
+ effective_env = dict(env or {})
1232
1244
  return self.start(
1233
1245
  agent_id,
1234
1246
  config=config,
1235
- env=env,
1247
+ env=effective_env,
1236
1248
  ports=ports,
1237
1249
  routes=_resolve_openclaw_routes(
1238
1250
  routes,
@@ -1250,6 +1262,35 @@ class Deployments:
1250
1262
  dry_run=dry_run,
1251
1263
  )
1252
1264
 
1265
+ def update(
1266
+ self,
1267
+ agent_id: str,
1268
+ *,
1269
+ name: str | None = None,
1270
+ size: str | None = None,
1271
+ refresh_from_lagoon: bool | None = None,
1272
+ last_error: str | None = None,
1273
+ ) -> Agent:
1274
+ body: dict[str, Any] = {}
1275
+ if name is not None:
1276
+ body["name"] = name
1277
+ if size is not None:
1278
+ body["size"] = size
1279
+ if refresh_from_lagoon is not None:
1280
+ body["refresh_from_lagoon"] = refresh_from_lagoon
1281
+ if last_error is not None:
1282
+ body["last_error"] = last_error
1283
+ data = self._http.patch(f"{AGENTS_API_PREFIX}/{agent_id}", json=body)
1284
+ return self._hydrate_agent(data)
1285
+
1286
+ def resize(
1287
+ self,
1288
+ agent_id: str,
1289
+ *,
1290
+ size: str | None = None,
1291
+ ) -> Agent:
1292
+ return self.update(agent_id, size=size)
1293
+
1253
1294
  def stop(self, agent_id: str) -> Agent:
1254
1295
  """Stop an agent (tears down pod, keeps DB record).
1255
1296
 
@@ -19,6 +19,7 @@ from .voice import VoiceAPI
19
19
  from .agents import Deployments
20
20
  from .agent import HyperAgent
21
21
  from .keys import KeysAPI
22
+ from .models import ModelsAPI
22
23
 
23
24
 
24
25
  def _derive_agents_api_base(api_url: str, agent_dev: bool) -> str:
@@ -100,6 +101,7 @@ class HyperCLI:
100
101
  self.files = Files(self._http)
101
102
  self.voice = VoiceAPI(self._http)
102
103
  self.keys = KeysAPI(self._http)
104
+ self.models = ModelsAPI(self._http)
103
105
  self.agent = HyperAgent(
104
106
  self._http,
105
107
  agent_api_key=resolved_agent_api_key,
@@ -0,0 +1,7 @@
1
+ """Backward-compatible module alias for OpenClaw gateway surfaces."""
2
+
3
+ import sys
4
+
5
+ from .openclaw import gateway as _gateway
6
+
7
+ sys.modules[__name__] = _gateway
@@ -166,10 +166,10 @@ class HTTPClient:
166
166
  )
167
167
  return _handle_response(resp)
168
168
 
169
- def post_bytes(self, path: str, json: dict = None) -> bytes:
169
+ def post_bytes(self, path: str, json: dict = None, timeout: float | None = None) -> bytes:
170
170
  resp = request_with_retry(
171
171
  "post", f"{self.base_url}{path}",
172
- headers=self.headers, timeout=self.timeout, json=json
172
+ headers=self.headers, timeout=timeout if timeout is not None else self.timeout, json=json
173
173
  )
174
174
  return _handle_bytes_response(resp)
175
175
 
@@ -0,0 +1,33 @@
1
+ """OpenAI-compatible models API"""
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, List
4
+
5
+ if TYPE_CHECKING:
6
+ from .http import HTTPClient
7
+
8
+
9
+ @dataclass
10
+ class Model:
11
+ id: str
12
+ object: str
13
+ owned_by: str | None = None
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: dict) -> "Model":
17
+ return cls(
18
+ id=data.get("id", ""),
19
+ object=data.get("object", "model"),
20
+ owned_by=data.get("owned_by"),
21
+ )
22
+
23
+
24
+ class ModelsAPI:
25
+ """OpenAI-compatible models API"""
26
+
27
+ def __init__(self, http: "HTTPClient"):
28
+ self._http = http
29
+
30
+ def list(self) -> List[Model]:
31
+ payload = self._http.get("/v1/models")
32
+ data = payload.get("data") if isinstance(payload, dict) else payload
33
+ return [Model.from_dict(item) for item in (data or [])]
@@ -0,0 +1,25 @@
1
+ """OpenClaw-specific SDK surfaces."""
2
+
3
+ from .gateway import (
4
+ GatewayClient,
5
+ GatewayError,
6
+ ChatEvent,
7
+ GatewayChatToolCall,
8
+ GatewayChatMessageSummary,
9
+ extract_gateway_chat_thinking,
10
+ extract_gateway_chat_media_urls,
11
+ extract_gateway_chat_tool_calls,
12
+ normalize_gateway_chat_message,
13
+ )
14
+
15
+ __all__ = [
16
+ "GatewayClient",
17
+ "GatewayError",
18
+ "ChatEvent",
19
+ "GatewayChatToolCall",
20
+ "GatewayChatMessageSummary",
21
+ "extract_gateway_chat_thinking",
22
+ "extract_gateway_chat_media_urls",
23
+ "extract_gateway_chat_tool_calls",
24
+ "normalize_gateway_chat_message",
25
+ ]
@@ -14,12 +14,13 @@ import base64
14
14
  import hashlib
15
15
  import json
16
16
  import os
17
+ import shlex
17
18
  import time
18
19
  import uuid
19
20
  from dataclasses import asdict, dataclass
20
21
  from pathlib import Path
21
22
  from typing import Any, AsyncIterator, Callable, Literal, Optional
22
- from urllib.parse import parse_qsl, quote, urlsplit
23
+ from urllib.parse import parse_qsl, quote, urlsplit, urlunsplit
23
24
 
24
25
  import httpx
25
26
  import websockets
@@ -815,6 +816,10 @@ class GatewayClient:
815
816
  raise RuntimeError(
816
817
  "auto_approve_pairing requires deployment_id, api_key, and api_base"
817
818
  )
819
+ command = (
820
+ "openclaw devices approve "
821
+ f"{shlex.quote(request_id)} --json"
822
+ )
818
823
  async with httpx.AsyncClient(timeout=30) as client:
819
824
  response = await client.post(
820
825
  f"{self.api_base}/deployments/{quote(self.deployment_id or '')}/exec",
@@ -823,7 +828,7 @@ class GatewayClient:
823
828
  "Content-Type": "application/json",
824
829
  },
825
830
  json={
826
- "command": f"openclaw devices approve {request_id}",
831
+ "command": command,
827
832
  "timeout": 30,
828
833
  },
829
834
  )