hypercli-sdk 2026.4.5__tar.gz → 2026.4.7__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 (48) hide show
  1. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/PKG-INFO +1 -1
  2. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/__init__.py +12 -2
  3. hypercli_sdk-2026.4.7/hypercli/agent.py +386 -0
  4. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/agents.py +8 -0
  5. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/client.py +2 -3
  6. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/gateway.py +3 -0
  7. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/renders.py +4 -4
  8. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/user.py +1 -1
  9. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/pyproject.toml +1 -1
  10. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_agents.py +41 -7
  11. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_agents.py +79 -0
  12. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_claw.py +134 -1
  13. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_gateway.py +10 -0
  14. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_keys.py +2 -6
  15. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_renders_subscription.py +7 -7
  16. hypercli_sdk-2026.4.5/hypercli/agent.py +0 -215
  17. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/.gitignore +0 -0
  18. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/README.md +0 -0
  19. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/billing.py +0 -0
  20. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/config.py +0 -0
  21. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/files.py +0 -0
  22. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/http.py +0 -0
  23. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/instances.py +0 -0
  24. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/__init__.py +0 -0
  25. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/base.py +0 -0
  26. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/comfyui.py +0 -0
  27. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/gradio.py +0 -0
  28. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/jobs.py +0 -0
  29. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/keys.py +0 -0
  30. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/logs.py +0 -0
  31. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/shell.py +0 -0
  32. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/voice.py +0 -0
  33. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/x402.py +0 -0
  34. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/conftest.py +0 -0
  35. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_auth.py +0 -0
  36. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_billing.py +0 -0
  37. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_instances.py +0 -0
  38. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_jobs_dryrun.py +0 -0
  39. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_keys.py +0 -0
  40. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_renders.py +0 -0
  41. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_apply_params.py +0 -0
  42. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_config.py +0 -0
  43. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_exec_shell_dryrun.py +0 -0
  44. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_gateway_retry.py +0 -0
  45. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_graph_to_api.py +0 -0
  46. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_http.py +0 -0
  47. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_jobs.py +0 -0
  48. {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_voice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 2026.4.5
3
+ Version: 2026.4.7
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
@@ -38,7 +38,14 @@ from .job import BaseJob, ComfyUIJob, GradioJob, apply_params, apply_graph_modes
38
38
  from .logs import LogStream, stream_logs, fetch_logs
39
39
  from .agents import Deployments, Agent, OpenClawAgent, ExecResult, build_openclaw_routes
40
40
  from .shell import ShellSession, shell_connect
41
- from .agent import HyperAgent, HyperAgentPlan, HyperAgentModel
41
+ from .agent import (
42
+ HyperAgent,
43
+ HyperAgentPlan,
44
+ HyperAgentCurrentPlan,
45
+ HyperAgentSubscription,
46
+ HyperAgentSubscriptionSummary,
47
+ HyperAgentModel,
48
+ )
42
49
  from .gateway import (
43
50
  GatewayClient,
44
51
  GatewayError,
@@ -50,7 +57,7 @@ from .gateway import (
50
57
  extract_gateway_chat_tool_calls,
51
58
  normalize_gateway_chat_message,
52
59
  )
53
- __version__ = "2026.4.5"
60
+ __version__ = "2026.4.7"
54
61
  __all__ = [
55
62
  "HyperCLI",
56
63
  "configure",
@@ -130,6 +137,9 @@ __all__ = [
130
137
  # HyperAgent
131
138
  "HyperAgent",
132
139
  "HyperAgentPlan",
140
+ "HyperAgentCurrentPlan",
141
+ "HyperAgentSubscription",
142
+ "HyperAgentSubscriptionSummary",
133
143
  "HyperAgentModel",
134
144
  # OpenClaw Gateway
135
145
  "GatewayClient",
@@ -0,0 +1,386 @@
1
+ """
2
+ HyperAgent API client
3
+
4
+ Provides access to the HyperClaw inference API for AI agents.
5
+ Uses the official OpenAI Python client for chat completions.
6
+ """
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional, Union
10
+ from urllib.parse import urlsplit
11
+
12
+ from .config import get_agents_api_base_url
13
+ from .http import HTTPClient
14
+
15
+ try:
16
+ from openai import OpenAI
17
+
18
+ OPENAI_AVAILABLE = True
19
+ except ImportError:
20
+ OpenAI = None
21
+ OPENAI_AVAILABLE = False
22
+
23
+
24
+ @dataclass
25
+ class HyperAgentPlan:
26
+ """HyperAgent subscription plan."""
27
+
28
+ id: str
29
+ name: str
30
+ price_usd: float
31
+ tpm_limit: int
32
+ rpm_limit: int
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: dict) -> "HyperAgentPlan":
36
+ price = data.get("price_usd", data.get("price", 0))
37
+ return cls(
38
+ id=data["id"],
39
+ name=data.get("name", data["id"]),
40
+ price_usd=float(price or 0),
41
+ tpm_limit=int(data.get("tpm_limit", 0)),
42
+ rpm_limit=int(data.get("rpm_limit", 0)),
43
+ )
44
+
45
+
46
+ @dataclass
47
+ class HyperAgentCurrentPlan:
48
+ """Effective current plan snapshot for an authenticated HyperClaw user."""
49
+
50
+ id: str
51
+ name: str
52
+ price: float | str
53
+ aiu: int | None = None
54
+ agents: int | None = None
55
+ tpm_limit: int = 0
56
+ rpm_limit: int = 0
57
+ expires_at: datetime | None = None
58
+ cancel_at_period_end: bool = False
59
+ provider: str | None = None
60
+ seconds_remaining: int | None = None
61
+ pooled_tpd: int = 0
62
+ slot_inventory: dict[str, Any] | None = None
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict) -> "HyperAgentCurrentPlan":
66
+ expires_at = data.get("expires_at")
67
+ if expires_at:
68
+ expires_at = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
69
+ return cls(
70
+ id=data["id"],
71
+ name=data.get("name", data["id"]),
72
+ price=data.get("price", 0),
73
+ aiu=data.get("aiu"),
74
+ agents=data.get("agents"),
75
+ tpm_limit=int(data.get("tpm_limit", 0) or 0),
76
+ rpm_limit=int(data.get("rpm_limit", 0) or 0),
77
+ expires_at=expires_at,
78
+ cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
79
+ provider=data.get("provider"),
80
+ seconds_remaining=data.get("seconds_remaining"),
81
+ pooled_tpd=int(data.get("pooled_tpd", 0) or 0),
82
+ slot_inventory=data.get("slot_inventory") or None,
83
+ )
84
+
85
+
86
+ @dataclass
87
+ class HyperAgentSubscription:
88
+ """A purchased HyperClaw entitlement/subscription instance."""
89
+
90
+ id: str
91
+ user_id: str
92
+ plan_id: str
93
+ plan_name: str
94
+ provider: str
95
+ status: str
96
+ quantity: int = 1
97
+ expires_at: datetime | None = None
98
+ updated_at: datetime | None = None
99
+ stripe_subscription_id: str | None = None
100
+ cancel_at_period_end: bool = False
101
+ can_cancel: bool = False
102
+ is_current: bool = False
103
+ meta: dict[str, Any] | None = None
104
+ plan_tpm_limit: int = 0
105
+ plan_rpm_limit: int = 0
106
+ plan_tpd: int = 0
107
+ plan_agent_tier: str | None = None
108
+ slot_grants: dict[str, int] | None = None
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict) -> "HyperAgentSubscription":
112
+ expires_at = data.get("expires_at")
113
+ updated_at = data.get("updated_at")
114
+ return cls(
115
+ id=data["id"],
116
+ user_id=data.get("user_id", ""),
117
+ plan_id=data.get("plan_id", ""),
118
+ plan_name=data.get("plan_name", data.get("plan_id", "")),
119
+ provider=data.get("provider", ""),
120
+ status=data.get("status", ""),
121
+ quantity=int(data.get("quantity", 1) or 1),
122
+ expires_at=datetime.fromisoformat(str(expires_at).replace("Z", "+00:00")) if expires_at else None,
123
+ updated_at=datetime.fromisoformat(str(updated_at).replace("Z", "+00:00")) if updated_at else None,
124
+ stripe_subscription_id=data.get("stripe_subscription_id"),
125
+ cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
126
+ can_cancel=bool(data.get("can_cancel", False)),
127
+ is_current=bool(data.get("is_current", False)),
128
+ meta=data.get("meta") or None,
129
+ plan_tpm_limit=int(data.get("plan_tpm_limit", 0) or 0),
130
+ plan_rpm_limit=int(data.get("plan_rpm_limit", 0) or 0),
131
+ plan_tpd=int(data.get("plan_tpd", 0) or 0),
132
+ plan_agent_tier=data.get("plan_agent_tier"),
133
+ slot_grants=data.get("slot_grants") or None,
134
+ )
135
+
136
+
137
+ @dataclass
138
+ class HyperAgentSubscriptionSummary:
139
+ """Effective entitlement summary for an authenticated HyperClaw user."""
140
+
141
+ effective_plan_id: str
142
+ current_subscription_id: str | None
143
+ pooled_tpm_limit: int
144
+ pooled_rpm_limit: int
145
+ pooled_tpd: int
146
+ slot_inventory: dict[str, Any]
147
+ active_subscription_count: int
148
+ active_subscriptions: list[HyperAgentSubscription]
149
+ subscriptions: list[HyperAgentSubscription]
150
+ user: dict[str, Any]
151
+
152
+ @classmethod
153
+ def from_dict(cls, data: dict) -> "HyperAgentSubscriptionSummary":
154
+ return cls(
155
+ effective_plan_id=data.get("effective_plan_id", ""),
156
+ current_subscription_id=data.get("current_subscription_id"),
157
+ pooled_tpm_limit=int(data.get("pooled_tpm_limit", 0) or 0),
158
+ pooled_rpm_limit=int(data.get("pooled_rpm_limit", 0) or 0),
159
+ pooled_tpd=int(data.get("pooled_tpd", 0) or 0),
160
+ slot_inventory=data.get("slot_inventory") or {},
161
+ active_subscription_count=int(data.get("active_subscription_count", 0) or 0),
162
+ active_subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("active_subscriptions", [])],
163
+ subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("subscriptions", [])],
164
+ user=data.get("user") or {},
165
+ )
166
+
167
+
168
+ @dataclass
169
+ class HyperAgentModel:
170
+ """Available model on HyperAgent."""
171
+
172
+ id: str
173
+ name: str
174
+ context_length: int
175
+ supports_vision: bool = False
176
+ supports_function_calling: bool = False
177
+ supports_tool_choice: bool = False
178
+
179
+ @classmethod
180
+ def from_dict(cls, data: dict) -> "HyperAgentModel":
181
+ caps = data.get("capabilities", {})
182
+ return cls(
183
+ id=data["id"],
184
+ name=data.get("name", data["id"]),
185
+ context_length=data.get("context_length", 0),
186
+ supports_vision=caps.get("supports_vision", False),
187
+ supports_function_calling=caps.get("supports_function_calling", False),
188
+ supports_tool_choice=caps.get("supports_tool_choice", False),
189
+ )
190
+
191
+
192
+ class HyperAgent:
193
+ """
194
+ HyperAgent API client.
195
+
196
+ Provides access to HyperClaw inference endpoints using the OpenAI Python
197
+ client.
198
+
199
+ Usage:
200
+ from hypercli import HyperCLI
201
+
202
+ client = HyperCLI(agent_api_key="sk-...")
203
+
204
+ openai = client.agent.openai
205
+ response = openai.chat.completions.create(
206
+ model="kimi-k2.5",
207
+ messages=[{"role": "user", "content": "Hello!"}],
208
+ )
209
+
210
+ response = client.agent.chat(
211
+ model="kimi-k2.5",
212
+ messages=[{"role": "user", "content": "Hello!"}],
213
+ )
214
+ """
215
+
216
+ AGENT_API_BASE = "https://api.hypercli.com/v1"
217
+ DEV_API_BASE = "https://api.dev.hypercli.com/v1"
218
+
219
+ def __init__(
220
+ self,
221
+ http: HTTPClient,
222
+ agent_api_key: str = None,
223
+ dev: bool = False,
224
+ agents_api_base_url: str | None = None,
225
+ ):
226
+ self._http = http
227
+ self._api_key = agent_api_key or http.api_key
228
+ self._dev = dev
229
+ self._base_url = self._resolve_base_url(agents_api_base_url, dev)
230
+ self._control_base_url = self._resolve_control_base_url(getattr(http, "base_url", None), agents_api_base_url, dev)
231
+ self._openai = None
232
+
233
+ @classmethod
234
+ def _resolve_base_url(cls, agents_api_base_url: str | None, dev: bool) -> str:
235
+ raw = (agents_api_base_url or "").rstrip("/")
236
+ if not raw:
237
+ fallback = get_agents_api_base_url(dev).rstrip("/")
238
+ return cls._resolve_base_url(fallback, dev)
239
+ parsed = urlsplit(raw if "://" in raw else f"https://{raw}")
240
+ host = parsed.netloc.lower()
241
+ if host in {"api.hypercli.com", "api.hyperclaw.app", "api.agents.hypercli.com"}:
242
+ return "https://api.agents.hypercli.com/v1"
243
+ if host in {"api.dev.hypercli.com", "api.dev.hyperclaw.app", "dev-api.hyperclaw.app", "api.agents.dev.hypercli.com"}:
244
+ return "https://api.agents.dev.hypercli.com/v1"
245
+ if raw.endswith("/api"):
246
+ return f"{raw[:-4]}/v1"
247
+ if raw.endswith("/agents"):
248
+ return f"{raw[:-7]}/v1"
249
+ if raw:
250
+ return f"{raw}/v1"
251
+ return cls.DEV_API_BASE if dev else cls.AGENT_API_BASE
252
+
253
+ @classmethod
254
+ def _resolve_control_base_url(
255
+ cls,
256
+ product_api_base_url: str | None,
257
+ agents_api_base_url: str | None,
258
+ dev: bool,
259
+ ) -> str:
260
+ raw_agents = (agents_api_base_url or "").rstrip("/")
261
+ if not raw_agents:
262
+ fallback = get_agents_api_base_url(dev).rstrip("/")
263
+ return cls._resolve_control_base_url(None, fallback, dev)
264
+ parsed = urlsplit(raw_agents if "://" in raw_agents else f"https://{raw_agents}")
265
+ scheme = parsed.scheme or "https"
266
+ normalized_path = parsed.path.rstrip("/")
267
+ host = parsed.netloc.lower()
268
+ if normalized_path.endswith("/agents"):
269
+ return f"{scheme}://{parsed.netloc}{normalized_path}"
270
+ if host in {"api.hypercli.com", "api.hyperclaw.app", "api.agents.hypercli.com"}:
271
+ return "https://api.hypercli.com/agents"
272
+ if host in {"api.dev.hypercli.com", "api.dev.hyperclaw.app", "dev-api.hyperclaw.app", "api.agents.dev.hypercli.com"}:
273
+ return "https://api.dev.hypercli.com/agents"
274
+ return f"{scheme}://{parsed.netloc}/agents"
275
+
276
+ @property
277
+ def openai(self) -> "OpenAI":
278
+ if not OPENAI_AVAILABLE:
279
+ raise ImportError(
280
+ "OpenAI package required for chat. Install with: pip install openai"
281
+ )
282
+
283
+ if self._openai is None:
284
+ self._openai = OpenAI(
285
+ api_key=self._api_key,
286
+ base_url=self._base_url,
287
+ )
288
+ return self._openai
289
+
290
+ def chat(
291
+ self,
292
+ model: str,
293
+ messages: List[Dict],
294
+ temperature: float = None,
295
+ max_tokens: int = None,
296
+ tools: List[Dict] = None,
297
+ tool_choice: Union[str, Dict] = None,
298
+ stream: bool = False,
299
+ **kwargs,
300
+ ):
301
+ params = {
302
+ "model": model,
303
+ "messages": messages,
304
+ **kwargs,
305
+ }
306
+
307
+ if temperature is not None:
308
+ params["temperature"] = temperature
309
+ if max_tokens is not None:
310
+ params["max_tokens"] = max_tokens
311
+ if tools:
312
+ params["tools"] = tools
313
+ if tool_choice:
314
+ params["tool_choice"] = tool_choice
315
+ if stream:
316
+ params["stream"] = stream
317
+
318
+ return self.openai.chat.completions.create(**params)
319
+
320
+ def models(self) -> List[HyperAgentModel]:
321
+ response = self.openai.models.list()
322
+ return [
323
+ HyperAgentModel.from_dict(
324
+ {
325
+ "id": model.id,
326
+ "name": getattr(model, "name", model.id),
327
+ "context_length": getattr(model, "context_length", 0),
328
+ "capabilities": getattr(model, "capabilities", {}),
329
+ }
330
+ )
331
+ for model in response.data
332
+ ]
333
+
334
+ def _api_base_without_v1(self) -> str:
335
+ return self._base_url.replace("/v1", "")
336
+
337
+ def plans(self) -> List[HyperAgentPlan]:
338
+ response = self._http._session.get(
339
+ f"{self._control_base_url}/plans",
340
+ headers={"Authorization": f"Bearer {self._api_key}"},
341
+ )
342
+ response.raise_for_status()
343
+ data = response.json()
344
+ return [HyperAgentPlan.from_dict(plan) for plan in data.get("plans", [])]
345
+
346
+ def current_plan(self) -> HyperAgentCurrentPlan:
347
+ response = self._http._session.get(
348
+ f"{self._control_base_url}/plans/current",
349
+ headers={"Authorization": f"Bearer {self._api_key}"},
350
+ )
351
+ response.raise_for_status()
352
+ return HyperAgentCurrentPlan.from_dict(response.json())
353
+
354
+ def subscriptions(self) -> list[HyperAgentSubscription]:
355
+ response = self._http._session.get(
356
+ f"{self._control_base_url}/subscriptions",
357
+ headers={"Authorization": f"Bearer {self._api_key}"},
358
+ )
359
+ response.raise_for_status()
360
+ data = response.json()
361
+ return [HyperAgentSubscription.from_dict(item) for item in data.get("items", [])]
362
+
363
+ def subscription_summary(self) -> HyperAgentSubscriptionSummary:
364
+ response = self._http._session.get(
365
+ f"{self._control_base_url}/subscriptions/summary",
366
+ headers={"Authorization": f"Bearer {self._api_key}"},
367
+ )
368
+ response.raise_for_status()
369
+ return HyperAgentSubscriptionSummary.from_dict(response.json())
370
+
371
+ def discovery_health(self) -> Dict[str, Any]:
372
+ response = self._http._session.get(f"{self._api_base_without_v1()}/discovery/health")
373
+ response.raise_for_status()
374
+ return response.json()
375
+
376
+ def discovery_config(self, api_key: str = None) -> Dict[str, Any]:
377
+ headers = {}
378
+ if api_key:
379
+ headers["X-API-KEY"] = api_key
380
+
381
+ response = self._http._session.get(
382
+ f"{self._api_base_without_v1()}/discovery/config",
383
+ headers=headers,
384
+ )
385
+ response.raise_for_status()
386
+ return response.json()
@@ -258,6 +258,7 @@ def _deep_merge_config(base: dict[str, Any], patch: dict[str, Any]) -> dict[str,
258
258
 
259
259
 
260
260
  def _agent_kwargs_from_dict(data: dict) -> dict[str, Any]:
261
+ meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
261
262
  return {
262
263
  "id": data.get("id", ""),
263
264
  "user_id": data.get("user_id", ""),
@@ -277,6 +278,7 @@ def _agent_kwargs_from_dict(data: dict) -> dict[str, Any]:
277
278
  "created_at": _parse_dt(data.get("created_at")),
278
279
  "updated_at": _parse_dt(data.get("updated_at")),
279
280
  "launch_config": data.get("launch_config"),
281
+ "meta_ui": copy.deepcopy(meta.get("ui")) if isinstance(meta.get("ui"), dict) else None,
280
282
  "routes": data.get("routes") or {},
281
283
  "command": data.get("command") or [],
282
284
  "entrypoint": data.get("entrypoint") or [],
@@ -306,6 +308,7 @@ class Agent:
306
308
  created_at: Optional[datetime] = None
307
309
  updated_at: Optional[datetime] = None
308
310
  launch_config: Optional[dict] = None
311
+ meta_ui: Optional[dict] = None
309
312
  routes: dict[str, dict] = field(default_factory=dict)
310
313
  command: list[str] = field(default_factory=list)
311
314
  entrypoint: list[str] = field(default_factory=list)
@@ -992,6 +995,7 @@ class Deployments:
992
995
  registry_url: str = None,
993
996
  registry_auth: dict = None,
994
997
  gateway_token: str = None,
998
+ meta_ui: dict = None,
995
999
  dry_run: bool = False,
996
1000
  start: bool = True,
997
1001
  ) -> Agent:
@@ -1035,6 +1039,8 @@ class Deployments:
1035
1039
  body["cpu"] = cpu
1036
1040
  if memory is not None:
1037
1041
  body["memory"] = memory
1042
+ if meta_ui:
1043
+ body["meta"] = {"ui": copy.deepcopy(meta_ui)}
1038
1044
  if tags:
1039
1045
  body["tags"] = list(tags)
1040
1046
  data = self._post(AGENTS_API_PREFIX, json=body)
@@ -1065,6 +1071,7 @@ class Deployments:
1065
1071
  registry_url: str = None,
1066
1072
  registry_auth: dict = None,
1067
1073
  gateway_token: str = None,
1074
+ meta_ui: dict = None,
1068
1075
  dry_run: bool = False,
1069
1076
  start: bool = True,
1070
1077
  openclaw_routes: dict | None = None,
@@ -1092,6 +1099,7 @@ class Deployments:
1092
1099
  registry_url=registry_url,
1093
1100
  registry_auth=registry_auth,
1094
1101
  gateway_token=gateway_token,
1102
+ meta_ui=meta_ui,
1095
1103
  dry_run=dry_run,
1096
1104
  start=start,
1097
1105
  )
@@ -86,7 +86,6 @@ class HyperCLI:
86
86
  agents_ws_url
87
87
  or (_derive_agents_ws_url(self._api_url, agent_dev) if api_url else get_agents_ws_url(agent_dev))
88
88
  )
89
- auth_http = HTTPClient(resolved_agents_api_base, self._api_key)
90
89
  self.deployments = Deployments(
91
90
  self._http,
92
91
  api_key=resolved_agent_api_key,
@@ -95,9 +94,9 @@ class HyperCLI:
95
94
  )
96
95
  self.billing = Billing(self._http)
97
96
  self.jobs = Jobs(self._http)
98
- self.user = UserAPI(self._http, auth_http=auth_http)
97
+ self.user = UserAPI(self._http)
99
98
  self.instances = Instances(self._http)
100
- self.renders = Renders(self._http, auth_http=auth_http)
99
+ self.renders = Renders(self._http)
101
100
  self.files = Files(self._http)
102
101
  self.voice = VoiceAPI(self._http)
103
102
  self.keys = KeysAPI(self._http)
@@ -783,6 +783,9 @@ class GatewayClient:
783
783
  def pending_pairing(self) -> GatewayPairingState | None:
784
784
  return self._pending_pairing
785
785
 
786
+ def set_gateway_token(self, token: str | None) -> None:
787
+ self.gateway_token = token.strip() if isinstance(token, str) and token.strip() else None
788
+
786
789
  def _storage_scope(self) -> str:
787
790
  return self.deployment_id or self.url
788
791
 
@@ -63,7 +63,7 @@ class Renders:
63
63
 
64
64
  def _auth_me(self) -> dict[str, Any]:
65
65
  if self._auth_me_cache is None:
66
- self._auth_me_cache = self._auth_http.get("/auth/me")
66
+ self._auth_me_cache = self._auth_http.get("/api/auth/me")
67
67
  return self._auth_me_cache
68
68
 
69
69
  def _supports_subscription_family(self, family: str, resource: str | None = None) -> bool:
@@ -83,7 +83,7 @@ class Renders:
83
83
  return False
84
84
 
85
85
  def _post_flow(self, flow_type: str, payload: dict[str, Any]) -> dict[str, Any]:
86
- primary = f"/agents/flow/{flow_type}" if self._supports_subscription_family("renders", flow_type) else f"/api/flow/{flow_type}"
86
+ primary = f"/agents/flow/{flow_type}" if self._supports_subscription_family("flows", flow_type) else f"/api/flow/{flow_type}"
87
87
  try:
88
88
  return self._http.post(primary, json=payload)
89
89
  except APIError as exc:
@@ -93,7 +93,7 @@ class Renders:
93
93
 
94
94
  def _get_render(self, render_id: str, *, status: bool = False) -> dict[str, Any]:
95
95
  suffix = "/status" if status else ""
96
- primary = f"/agents/flow/renders/{render_id}{suffix}" if self._supports_subscription_family("renders") else f"/api/renders/{render_id}{suffix}"
96
+ primary = f"/agents/flow/renders/{render_id}{suffix}" if self._supports_subscription_family("flows") else f"/api/renders/{render_id}{suffix}"
97
97
  try:
98
98
  return self._http.get(primary)
99
99
  except APIError as exc:
@@ -102,7 +102,7 @@ class Renders:
102
102
  raise
103
103
 
104
104
  def _delete_render(self, render_id: str) -> dict:
105
- primary = f"/agents/flow/renders/{render_id}" if self._supports_subscription_family("renders") else f"/api/renders/{render_id}"
105
+ primary = f"/agents/flow/renders/{render_id}" if self._supports_subscription_family("flows") else f"/api/renders/{render_id}"
106
106
  try:
107
107
  return self._http.delete(primary)
108
108
  except APIError as exc:
@@ -68,5 +68,5 @@ class UserAPI:
68
68
 
69
69
  def auth_me(self) -> AuthMe:
70
70
  """Resolve the current auth context, including key capabilities."""
71
- data = self._auth_http.get("/auth/me")
71
+ data = self._auth_http.get("/api/auth/me")
72
72
  return AuthMe.from_dict(data)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-sdk"
7
- version = "2026.4.5"
7
+ version = "2026.4.7"
8
8
  description = "Python SDK for HyperCLI - GPU orchestration and HyperAgent API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,6 +8,40 @@ from hypercli import HyperCLI
8
8
  from hypercli.http import APIError
9
9
 
10
10
 
11
+ def _create_agent_with_available_tier(client: HyperCLI, name: str, tags: list[str]) -> tuple[str, str]:
12
+ last_error: APIError | None = None
13
+ for tier in ("large", "medium", "small"):
14
+ agent_id: str | None = None
15
+ try:
16
+ agent = client.deployments.create(
17
+ name=name,
18
+ size=tier,
19
+ start=False,
20
+ tags=tags,
21
+ )
22
+ agent_id = agent.id
23
+ client.deployments.start_openclaw(agent.id, dry_run=True)
24
+ return agent.id, tier
25
+ except APIError as exc:
26
+ if exc.status_code == 429:
27
+ if agent_id:
28
+ try:
29
+ client.deployments.delete(agent_id)
30
+ except APIError:
31
+ pass
32
+ last_error = exc
33
+ continue
34
+ if agent_id:
35
+ try:
36
+ client.deployments.delete(agent_id)
37
+ except APIError:
38
+ pass
39
+ raise
40
+ if last_error is not None:
41
+ raise last_error
42
+ raise AssertionError("No available entitlement slots for integration agent tests")
43
+
44
+
11
45
  def test_list_agents_requires_agent_key(client, test_agent_api_key: str):
12
46
  if not test_agent_api_key:
13
47
  pytest.skip(
@@ -24,18 +58,18 @@ def test_exact_agent_child_key_is_scoped_to_one_agent(client, test_api_base: str
24
58
  "TEST_AGENT_API_KEY not set; the deployments and agent APIs do not accept the account-level TEST_API_KEY"
25
59
  )
26
60
 
27
- agent_a = client.deployments.create(
61
+ agent_a_id, agent_a_tier = _create_agent_with_available_tier(
62
+ client,
28
63
  name=f"sdk-scope-{uuid.uuid4().hex[:8]}",
29
- size="small",
30
- start=False,
31
64
  tags=["team=dev", "suite=sdk-integration"],
32
65
  )
33
- agent_b = client.deployments.create(
66
+ agent_b_id, _agent_b_tier = _create_agent_with_available_tier(
67
+ client,
34
68
  name=f"sdk-scope-{uuid.uuid4().hex[:8]}",
35
- size="small",
36
- start=False,
37
69
  tags=["team=ops", "suite=sdk-integration"],
38
70
  )
71
+ agent_a = client.deployments.get(agent_a_id)
72
+ agent_b = client.deployments.get(agent_b_id)
39
73
 
40
74
  try:
41
75
  child = client.deployments.create_scoped_key(agent_a.id, name="sdk-scoped-child")
@@ -64,7 +98,7 @@ def test_exact_agent_child_key_is_scoped_to_one_agent(client, test_api_base: str
64
98
  with pytest.raises(APIError) as create_exc:
65
99
  scoped.deployments.create(
66
100
  name=f"sdk-scope-{uuid.uuid4().hex[:8]}",
67
- size="small",
101
+ size=agent_a_tier,
68
102
  start=False,
69
103
  )
70
104
  assert create_exc.value.status_code == 403