hypercli-sdk 2026.4.6__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.6 → hypercli_sdk-2026.4.7}/PKG-INFO +1 -1
  2. {hypercli_sdk-2026.4.6 → 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.6 → hypercli_sdk-2026.4.7}/hypercli/agents.py +8 -0
  5. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/gateway.py +3 -0
  6. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/pyproject.toml +1 -1
  7. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_agents.py +41 -7
  8. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_agents.py +45 -0
  9. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_claw.py +134 -1
  10. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_gateway.py +10 -0
  11. hypercli_sdk-2026.4.6/hypercli/agent.py +0 -215
  12. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/.gitignore +0 -0
  13. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/README.md +0 -0
  14. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/billing.py +0 -0
  15. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/client.py +0 -0
  16. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/config.py +0 -0
  17. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/files.py +0 -0
  18. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/http.py +0 -0
  19. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/instances.py +0 -0
  20. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/__init__.py +0 -0
  21. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/base.py +0 -0
  22. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/comfyui.py +0 -0
  23. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/gradio.py +0 -0
  24. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/jobs.py +0 -0
  25. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/keys.py +0 -0
  26. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/logs.py +0 -0
  27. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/renders.py +0 -0
  28. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/shell.py +0 -0
  29. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/user.py +0 -0
  30. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/voice.py +0 -0
  31. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/x402.py +0 -0
  32. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/conftest.py +0 -0
  33. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_auth.py +0 -0
  34. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_billing.py +0 -0
  35. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_instances.py +0 -0
  36. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_jobs_dryrun.py +0 -0
  37. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_keys.py +0 -0
  38. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_renders.py +0 -0
  39. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_apply_params.py +0 -0
  40. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_config.py +0 -0
  41. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_exec_shell_dryrun.py +0 -0
  42. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_gateway_retry.py +0 -0
  43. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_graph_to_api.py +0 -0
  44. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_http.py +0 -0
  45. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_jobs.py +0 -0
  46. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_keys.py +0 -0
  47. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_renders_subscription.py +0 -0
  48. {hypercli_sdk-2026.4.6 → 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.6
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.6"
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
  )
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-sdk"
7
- version = "2026.4.6"
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
@@ -40,6 +40,36 @@ def test_agent_from_dict_minimal():
40
40
  assert agent.ports == []
41
41
 
42
42
 
43
+ def test_agent_from_dict_hydrates_only_meta_ui():
44
+ agent = Agent.from_dict(
45
+ {
46
+ "id": "agent-123",
47
+ "user_id": "user-456",
48
+ "pod_id": "pod-789",
49
+ "pod_name": "test-pod",
50
+ "state": "pending",
51
+ "meta": {
52
+ "ui": {
53
+ "avatar": {
54
+ "image": "data:image/png;base64,abc",
55
+ "icon_index": 4,
56
+ }
57
+ },
58
+ "internal": {
59
+ "ignored": True,
60
+ },
61
+ },
62
+ }
63
+ )
64
+
65
+ assert agent.meta_ui == {
66
+ "avatar": {
67
+ "image": "data:image/png;base64,abc",
68
+ "icon_index": 4,
69
+ }
70
+ }
71
+
72
+
43
73
  def test_agent_urls_and_running_state():
44
74
  agent = Agent(
45
75
  id="agent-123",
@@ -492,6 +522,12 @@ def test_agents_create_returns_openclaw_agent(agents_client):
492
522
  size="medium",
493
523
  cpu=4,
494
524
  memory=16,
525
+ meta_ui={
526
+ "avatar": {
527
+ "image": "data:image/png;base64,xyz",
528
+ "icon_index": 7,
529
+ }
530
+ },
495
531
  env={"FOO": "bar"},
496
532
  ports=[{"port": 18789, "auth": False}],
497
533
  command=["nginx", "-g", "daemon off;"],
@@ -507,6 +543,14 @@ def test_agents_create_returns_openclaw_agent(agents_client):
507
543
  "FOO": "bar",
508
544
  "OPENCLAW_GATEWAY_TOKEN": "gw-token-123",
509
545
  }
546
+ assert posted_json["meta"] == {
547
+ "ui": {
548
+ "avatar": {
549
+ "image": "data:image/png;base64,xyz",
550
+ "icon_index": 7,
551
+ }
552
+ }
553
+ }
510
554
  assert posted_json["command"] == ["nginx", "-g", "daemon off;"]
511
555
  assert posted_json["entrypoint"] == ["/docker-entrypoint.sh"]
512
556
  assert posted_json["image"] == "ghcr.io/hypercli/hypercli-openclaw:test"
@@ -515,6 +559,7 @@ def test_agents_create_returns_openclaw_agent(agents_client):
515
559
  assert isinstance(agent, OpenClawAgent)
516
560
  assert agent.gateway_token == "gw-token-123"
517
561
  assert agent.gateway_url == "wss://openclaw-test.hypercli.com"
562
+ assert agent.meta_ui is None
518
563
  assert agent._deployments is agents_client
519
564
 
520
565
 
@@ -5,7 +5,14 @@ import pytest
5
5
  import os
6
6
  from unittest.mock import Mock, patch, MagicMock
7
7
  from hypercli import HyperCLI
8
- from hypercli.agent import HyperAgent, HyperAgentPlan, HyperAgentModel
8
+ from hypercli.agent import (
9
+ HyperAgent,
10
+ HyperAgentPlan,
11
+ HyperAgentCurrentPlan,
12
+ HyperAgentSubscription,
13
+ HyperAgentSubscriptionSummary,
14
+ HyperAgentModel,
15
+ )
9
16
 
10
17
 
11
18
  class TestHyperAgentDataclasses:
@@ -40,6 +47,53 @@ class TestHyperAgentDataclasses:
40
47
  assert model.supports_vision is True
41
48
  assert model.supports_function_calling is True
42
49
 
50
+ def test_current_plan_from_dict(self):
51
+ current = HyperAgentCurrentPlan.from_dict(
52
+ {
53
+ "id": "large",
54
+ "name": "Large",
55
+ "price": 99,
56
+ "tpm_limit": 1000,
57
+ "rpm_limit": 10,
58
+ "expires_at": "2026-04-07T10:00:00Z",
59
+ "cancel_at_period_end": True,
60
+ "pooled_tpd": 1000000,
61
+ "slot_inventory": {"large": {"granted": 1, "used": 0, "available": 1}},
62
+ }
63
+ )
64
+ assert current.id == "large"
65
+ assert current.cancel_at_period_end is True
66
+ assert current.expires_at is not None
67
+ assert current.slot_inventory["large"]["granted"] == 1
68
+
69
+ def test_subscription_summary_from_dict(self):
70
+ summary = HyperAgentSubscriptionSummary.from_dict(
71
+ {
72
+ "effective_plan_id": "large",
73
+ "current_subscription_id": "sub-1",
74
+ "pooled_tpm_limit": 2000,
75
+ "pooled_rpm_limit": 20,
76
+ "pooled_tpd": 2000000,
77
+ "slot_inventory": {"large": {"granted": 2, "used": 1, "available": 1}},
78
+ "active_subscription_count": 1,
79
+ "active_subscriptions": [
80
+ {
81
+ "id": "sub-1",
82
+ "user_id": "user-1",
83
+ "plan_id": "large",
84
+ "plan_name": "Large",
85
+ "provider": "STRIPE",
86
+ "status": "ACTIVE",
87
+ }
88
+ ],
89
+ "subscriptions": [],
90
+ "user": {"id": "user-1", "team_id": "team-1"},
91
+ }
92
+ )
93
+ assert summary.effective_plan_id == "large"
94
+ assert summary.active_subscription_count == 1
95
+ assert summary.active_subscriptions[0].plan_id == "large"
96
+
43
97
 
44
98
  class TestHyperAgentClient:
45
99
  """Tests for HyperAgent client methods."""
@@ -66,6 +120,85 @@ class TestHyperAgentClient:
66
120
  assert result["status"] == "ok"
67
121
  assert result["hosts_total"] == 1
68
122
  mock_http._session.get.assert_called_once()
123
+
124
+ def test_current_plan(self, mock_http):
125
+ mock_http._session.get.return_value.json.return_value = {
126
+ "id": "large",
127
+ "name": "Large",
128
+ "price": 99,
129
+ "tpm_limit": 1000,
130
+ "rpm_limit": 10,
131
+ }
132
+ mock_http._session.get.return_value.raise_for_status = Mock()
133
+
134
+ agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
135
+ current = agent.current_plan()
136
+
137
+ assert current.id == "large"
138
+ mock_http._session.get.assert_called_with(
139
+ "https://api.hypercli.com/agents/plans/current",
140
+ headers={"Authorization": "Bearer sk-hyper-test"},
141
+ )
142
+
143
+ def test_subscriptions(self, mock_http):
144
+ mock_http._session.get.return_value.json.return_value = {
145
+ "items": [
146
+ {
147
+ "id": "sub-1",
148
+ "user_id": "user-1",
149
+ "plan_id": "large",
150
+ "plan_name": "Large",
151
+ "provider": "STRIPE",
152
+ "status": "ACTIVE",
153
+ "quantity": 2,
154
+ }
155
+ ]
156
+ }
157
+ mock_http._session.get.return_value.raise_for_status = Mock()
158
+
159
+ agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
160
+ subscriptions = agent.subscriptions()
161
+
162
+ assert len(subscriptions) == 1
163
+ assert subscriptions[0].quantity == 2
164
+ mock_http._session.get.assert_called_with(
165
+ "https://api.hypercli.com/agents/subscriptions",
166
+ headers={"Authorization": "Bearer sk-hyper-test"},
167
+ )
168
+
169
+ def test_subscription_summary(self, mock_http):
170
+ mock_http._session.get.return_value.json.return_value = {
171
+ "effective_plan_id": "large",
172
+ "current_subscription_id": "sub-1",
173
+ "pooled_tpm_limit": 2000,
174
+ "pooled_rpm_limit": 20,
175
+ "pooled_tpd": 2000000,
176
+ "slot_inventory": {"large": {"granted": 2, "used": 1, "available": 1}},
177
+ "active_subscription_count": 1,
178
+ "active_subscriptions": [
179
+ {
180
+ "id": "sub-1",
181
+ "user_id": "user-1",
182
+ "plan_id": "large",
183
+ "plan_name": "Large",
184
+ "provider": "STRIPE",
185
+ "status": "ACTIVE",
186
+ }
187
+ ],
188
+ "subscriptions": [],
189
+ "user": {"id": "user-1", "team_id": "team-1"},
190
+ }
191
+ mock_http._session.get.return_value.raise_for_status = Mock()
192
+
193
+ agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
194
+ summary = agent.subscription_summary()
195
+
196
+ assert summary.current_subscription_id == "sub-1"
197
+ assert summary.slot_inventory["large"]["available"] == 1
198
+ mock_http._session.get.assert_called_with(
199
+ "https://api.hypercli.com/agents/subscriptions/summary",
200
+ headers={"Authorization": "Bearer sk-hyper-test"},
201
+ )
69
202
 
70
203
  def test_openai_client_creation(self, mock_http):
71
204
  """Test that OpenAI client is created with correct config."""
@@ -119,6 +119,16 @@ async def test_connect_auto_approves_pairing_and_reconnects(monkeypatch: pytest.
119
119
  assert client.pending_pairing is None
120
120
 
121
121
 
122
+ def test_set_gateway_token_normalizes_blank_values() -> None:
123
+ client = GatewayClient(url="wss://openclaw-agent.example", gateway_token="gw-token-1")
124
+
125
+ client.set_gateway_token(" gw-token-2 ")
126
+ assert client.gateway_token == "gw-token-2"
127
+
128
+ client.set_gateway_token(" ")
129
+ assert client.gateway_token is None
130
+
131
+
122
132
  @pytest.mark.asyncio
123
133
  async def test_chat_send_accepts_chat_content_and_done_events() -> None:
124
134
  client = GatewayClient(url="wss://openclaw-agent.example")
@@ -1,215 +0,0 @@
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 HyperAgentModel:
48
- """Available model on HyperAgent."""
49
-
50
- id: str
51
- name: str
52
- context_length: int
53
- supports_vision: bool = False
54
- supports_function_calling: bool = False
55
- supports_tool_choice: bool = False
56
-
57
- @classmethod
58
- def from_dict(cls, data: dict) -> "HyperAgentModel":
59
- caps = data.get("capabilities", {})
60
- return cls(
61
- id=data["id"],
62
- name=data.get("name", data["id"]),
63
- context_length=data.get("context_length", 0),
64
- supports_vision=caps.get("supports_vision", False),
65
- supports_function_calling=caps.get("supports_function_calling", False),
66
- supports_tool_choice=caps.get("supports_tool_choice", False),
67
- )
68
-
69
-
70
- class HyperAgent:
71
- """
72
- HyperAgent API client.
73
-
74
- Provides access to HyperClaw inference endpoints using the OpenAI Python
75
- client.
76
-
77
- Usage:
78
- from hypercli import HyperCLI
79
-
80
- client = HyperCLI(agent_api_key="sk-...")
81
-
82
- openai = client.agent.openai
83
- response = openai.chat.completions.create(
84
- model="kimi-k2.5",
85
- messages=[{"role": "user", "content": "Hello!"}],
86
- )
87
-
88
- response = client.agent.chat(
89
- model="kimi-k2.5",
90
- messages=[{"role": "user", "content": "Hello!"}],
91
- )
92
- """
93
-
94
- AGENT_API_BASE = "https://api.hypercli.com/v1"
95
- DEV_API_BASE = "https://api.dev.hypercli.com/v1"
96
-
97
- def __init__(
98
- self,
99
- http: HTTPClient,
100
- agent_api_key: str = None,
101
- dev: bool = False,
102
- agents_api_base_url: str | None = None,
103
- ):
104
- self._http = http
105
- self._api_key = agent_api_key or http.api_key
106
- self._dev = dev
107
- self._base_url = self._resolve_base_url(agents_api_base_url, dev)
108
- self._openai = None
109
-
110
- @classmethod
111
- def _resolve_base_url(cls, agents_api_base_url: str | None, dev: bool) -> str:
112
- raw = (agents_api_base_url or "").rstrip("/")
113
- if not raw:
114
- fallback = get_agents_api_base_url(dev).rstrip("/")
115
- return cls._resolve_base_url(fallback, dev)
116
- parsed = urlsplit(raw if "://" in raw else f"https://{raw}")
117
- host = parsed.netloc.lower()
118
- if host in {"api.hypercli.com", "api.hyperclaw.app", "api.agents.hypercli.com"}:
119
- return "https://api.agents.hypercli.com/v1"
120
- if host in {"api.dev.hypercli.com", "api.dev.hyperclaw.app", "dev-api.hyperclaw.app", "api.agents.dev.hypercli.com"}:
121
- return "https://api.agents.dev.hypercli.com/v1"
122
- if raw.endswith("/api"):
123
- return f"{raw[:-4]}/v1"
124
- if raw.endswith("/agents"):
125
- return f"{raw[:-7]}/v1"
126
- if raw:
127
- return f"{raw}/v1"
128
- return cls.DEV_API_BASE if dev else cls.AGENT_API_BASE
129
-
130
- @property
131
- def openai(self) -> "OpenAI":
132
- if not OPENAI_AVAILABLE:
133
- raise ImportError(
134
- "OpenAI package required for chat. Install with: pip install openai"
135
- )
136
-
137
- if self._openai is None:
138
- self._openai = OpenAI(
139
- api_key=self._api_key,
140
- base_url=self._base_url,
141
- )
142
- return self._openai
143
-
144
- def chat(
145
- self,
146
- model: str,
147
- messages: List[Dict],
148
- temperature: float = None,
149
- max_tokens: int = None,
150
- tools: List[Dict] = None,
151
- tool_choice: Union[str, Dict] = None,
152
- stream: bool = False,
153
- **kwargs,
154
- ):
155
- params = {
156
- "model": model,
157
- "messages": messages,
158
- **kwargs,
159
- }
160
-
161
- if temperature is not None:
162
- params["temperature"] = temperature
163
- if max_tokens is not None:
164
- params["max_tokens"] = max_tokens
165
- if tools:
166
- params["tools"] = tools
167
- if tool_choice:
168
- params["tool_choice"] = tool_choice
169
- if stream:
170
- params["stream"] = stream
171
-
172
- return self.openai.chat.completions.create(**params)
173
-
174
- def models(self) -> List[HyperAgentModel]:
175
- response = self.openai.models.list()
176
- return [
177
- HyperAgentModel.from_dict(
178
- {
179
- "id": model.id,
180
- "name": getattr(model, "name", model.id),
181
- "context_length": getattr(model, "context_length", 0),
182
- "capabilities": getattr(model, "capabilities", {}),
183
- }
184
- )
185
- for model in response.data
186
- ]
187
-
188
- def _api_base_without_v1(self) -> str:
189
- return self._base_url.replace("/v1", "")
190
-
191
- def plans(self) -> List[HyperAgentPlan]:
192
- response = self._http._session.get(
193
- f"{self._api_base_without_v1()}/api/plans",
194
- headers={"Authorization": f"Bearer {self._api_key}"},
195
- )
196
- response.raise_for_status()
197
- data = response.json()
198
- return [HyperAgentPlan.from_dict(plan) for plan in data.get("plans", [])]
199
-
200
- def discovery_health(self) -> Dict[str, Any]:
201
- response = self._http._session.get(f"{self._api_base_without_v1()}/discovery/health")
202
- response.raise_for_status()
203
- return response.json()
204
-
205
- def discovery_config(self, api_key: str = None) -> Dict[str, Any]:
206
- headers = {}
207
- if api_key:
208
- headers["X-API-KEY"] = api_key
209
-
210
- response = self._http._session.get(
211
- f"{self._api_base_without_v1()}/discovery/config",
212
- headers=headers,
213
- )
214
- response.raise_for_status()
215
- return response.json()