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.
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/__init__.py +12 -2
- hypercli_sdk-2026.4.7/hypercli/agent.py +386 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/agents.py +8 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/client.py +2 -3
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/gateway.py +3 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/renders.py +4 -4
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/user.py +1 -1
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_agents.py +41 -7
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_agents.py +79 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_claw.py +134 -1
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_gateway.py +10 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_keys.py +2 -6
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_renders_subscription.py +7 -7
- hypercli_sdk-2026.4.5/hypercli/agent.py +0 -215
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/.gitignore +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/README.md +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/voice.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.5 → hypercli_sdk-2026.4.7}/tests/test_voice.py +0 -0
|
@@ -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
|
|
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.
|
|
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
|
|
97
|
+
self.user = UserAPI(self._http)
|
|
99
98
|
self.instances = Instances(self._http)
|
|
100
|
-
self.renders = Renders(self._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("
|
|
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("
|
|
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("
|
|
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:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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=
|
|
101
|
+
size=agent_a_tier,
|
|
68
102
|
start=False,
|
|
69
103
|
)
|
|
70
104
|
assert create_exc.value.status_code == 403
|