hypercli-sdk 2026.4.6__tar.gz → 2026.4.9__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 (51) hide show
  1. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/PKG-INFO +1 -1
  2. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/__init__.py +15 -2
  3. hypercli_sdk-2026.4.9/hypercli/agent.py +386 -0
  4. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/agents.py +71 -0
  5. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/client.py +2 -0
  6. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/gateway.py +3 -0
  7. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/http.py +2 -2
  8. hypercli_sdk-2026.4.9/hypercli/models.py +33 -0
  9. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/renders.py +67 -4
  10. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/voice.py +8 -0
  11. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/pyproject.toml +1 -1
  12. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_agents.py +39 -7
  13. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_keys.py +13 -2
  14. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_agents.py +116 -0
  15. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_claw.py +134 -1
  16. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_gateway.py +10 -0
  17. hypercli_sdk-2026.4.9/tests/test_models.py +27 -0
  18. hypercli_sdk-2026.4.9/tests/test_renders_subscription.py +188 -0
  19. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_voice.py +6 -3
  20. hypercli_sdk-2026.4.6/hypercli/agent.py +0 -215
  21. hypercli_sdk-2026.4.6/tests/test_renders_subscription.py +0 -86
  22. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/.gitignore +0 -0
  23. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/README.md +0 -0
  24. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/billing.py +0 -0
  25. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/config.py +0 -0
  26. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/files.py +0 -0
  27. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/instances.py +0 -0
  28. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/__init__.py +0 -0
  29. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/base.py +0 -0
  30. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/comfyui.py +0 -0
  31. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/gradio.py +0 -0
  32. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/jobs.py +0 -0
  33. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/keys.py +0 -0
  34. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/logs.py +0 -0
  35. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/shell.py +0 -0
  36. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/user.py +0 -0
  37. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/x402.py +0 -0
  38. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/conftest.py +0 -0
  39. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_auth.py +0 -0
  40. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_billing.py +0 -0
  41. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_instances.py +0 -0
  42. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_jobs_dryrun.py +0 -0
  43. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_renders.py +0 -0
  44. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_apply_params.py +0 -0
  45. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_config.py +0 -0
  46. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_exec_shell_dryrun.py +0 -0
  47. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_gateway_retry.py +0 -0
  48. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_graph_to_api.py +0 -0
  49. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_http.py +0 -0
  50. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_jobs.py +0 -0
  51. {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/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.6
3
+ Version: 2026.4.9
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
@@ -32,13 +32,21 @@ 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
38
39
  from .logs import LogStream, stream_logs, fetch_logs
39
40
  from .agents import Deployments, Agent, OpenClawAgent, ExecResult, build_openclaw_routes
40
41
  from .shell import ShellSession, shell_connect
41
- from .agent import HyperAgent, HyperAgentPlan, HyperAgentModel
42
+ from .agent import (
43
+ HyperAgent,
44
+ HyperAgentPlan,
45
+ HyperAgentCurrentPlan,
46
+ HyperAgentSubscription,
47
+ HyperAgentSubscriptionSummary,
48
+ HyperAgentModel,
49
+ )
42
50
  from .gateway import (
43
51
  GatewayClient,
44
52
  GatewayError,
@@ -50,7 +58,7 @@ from .gateway import (
50
58
  extract_gateway_chat_tool_calls,
51
59
  normalize_gateway_chat_message,
52
60
  )
53
- __version__ = "2026.4.6"
61
+ __version__ = "2026.4.7"
54
62
  __all__ = [
55
63
  "HyperCLI",
56
64
  "configure",
@@ -86,6 +94,8 @@ __all__ = [
86
94
  "Render",
87
95
  "RenderStatus",
88
96
  "VoiceAPI",
97
+ "Model",
98
+ "ModelsAPI",
89
99
  # x402 API
90
100
  "X402Client",
91
101
  "X402JobLaunch",
@@ -130,6 +140,9 @@ __all__ = [
130
140
  # HyperAgent
131
141
  "HyperAgent",
132
142
  "HyperAgentPlan",
143
+ "HyperAgentCurrentPlan",
144
+ "HyperAgentSubscription",
145
+ "HyperAgentSubscriptionSummary",
133
146
  "HyperAgentModel",
134
147
  # OpenClaw Gateway
135
148
  "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)
@@ -383,6 +386,32 @@ class Agent:
383
386
  self._deployments = agent._deployments
384
387
  return self
385
388
 
389
+ def update(
390
+ self,
391
+ *,
392
+ name: str | None = None,
393
+ size: str | None = None,
394
+ cpu: float | None = None,
395
+ memory: int | None = None,
396
+ refresh_from_lagoon: bool | None = None,
397
+ last_error: str | None = None,
398
+ ) -> "Agent":
399
+ agent = self._require_deployments().update(
400
+ self.id,
401
+ name=name,
402
+ size=size,
403
+ cpu=cpu,
404
+ memory=memory,
405
+ refresh_from_lagoon=refresh_from_lagoon,
406
+ last_error=last_error,
407
+ )
408
+ self.__dict__.update(agent.__dict__)
409
+ self._deployments = agent._deployments
410
+ return self
411
+
412
+ def resize(self, *, size: str | None = None, cpu: float | None = None, memory: int | None = None) -> "Agent":
413
+ return self.update(size=size, cpu=cpu, memory=memory)
414
+
386
415
  def env(self) -> dict[str, str]:
387
416
  """Fetch runtime environment from the pod's K8s secret."""
388
417
  data = self._require_deployments().env(self.id)
@@ -992,6 +1021,7 @@ class Deployments:
992
1021
  registry_url: str = None,
993
1022
  registry_auth: dict = None,
994
1023
  gateway_token: str = None,
1024
+ meta_ui: dict = None,
995
1025
  dry_run: bool = False,
996
1026
  start: bool = True,
997
1027
  ) -> Agent:
@@ -1035,6 +1065,8 @@ class Deployments:
1035
1065
  body["cpu"] = cpu
1036
1066
  if memory is not None:
1037
1067
  body["memory"] = memory
1068
+ if meta_ui:
1069
+ body["meta"] = {"ui": copy.deepcopy(meta_ui)}
1038
1070
  if tags:
1039
1071
  body["tags"] = list(tags)
1040
1072
  data = self._post(AGENTS_API_PREFIX, json=body)
@@ -1065,6 +1097,7 @@ class Deployments:
1065
1097
  registry_url: str = None,
1066
1098
  registry_auth: dict = None,
1067
1099
  gateway_token: str = None,
1100
+ meta_ui: dict = None,
1068
1101
  dry_run: bool = False,
1069
1102
  start: bool = True,
1070
1103
  openclaw_routes: dict | None = None,
@@ -1092,6 +1125,7 @@ class Deployments:
1092
1125
  registry_url=registry_url,
1093
1126
  registry_auth=registry_auth,
1094
1127
  gateway_token=gateway_token,
1128
+ meta_ui=meta_ui,
1095
1129
  dry_run=dry_run,
1096
1130
  start=start,
1097
1131
  )
@@ -1242,6 +1276,43 @@ class Deployments:
1242
1276
  dry_run=dry_run,
1243
1277
  )
1244
1278
 
1279
+ def update(
1280
+ self,
1281
+ agent_id: str,
1282
+ *,
1283
+ name: str | None = None,
1284
+ size: str | None = None,
1285
+ cpu: float | None = None,
1286
+ memory: int | None = None,
1287
+ refresh_from_lagoon: bool | None = None,
1288
+ last_error: str | None = None,
1289
+ ) -> Agent:
1290
+ body: dict[str, Any] = {}
1291
+ if name is not None:
1292
+ body["name"] = name
1293
+ if size is not None:
1294
+ body["size"] = size
1295
+ if cpu is not None:
1296
+ body["cpu"] = cpu
1297
+ if memory is not None:
1298
+ body["memory"] = memory
1299
+ if refresh_from_lagoon is not None:
1300
+ body["refresh_from_lagoon"] = refresh_from_lagoon
1301
+ if last_error is not None:
1302
+ body["last_error"] = last_error
1303
+ data = self._http.patch(f"{AGENTS_API_PREFIX}/{agent_id}", json=body)
1304
+ return self._hydrate_agent(data)
1305
+
1306
+ def resize(
1307
+ self,
1308
+ agent_id: str,
1309
+ *,
1310
+ size: str | None = None,
1311
+ cpu: float | None = None,
1312
+ memory: int | None = None,
1313
+ ) -> Agent:
1314
+ return self.update(agent_id, size=size, cpu=cpu, memory=memory)
1315
+
1245
1316
  def stop(self, agent_id: str) -> Agent:
1246
1317
  """Stop an agent (tears down pod, keeps DB record).
1247
1318
 
@@ -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,
@@ -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
 
@@ -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 [])]