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.
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/__init__.py +12 -2
- hypercli_sdk-2026.4.7/hypercli/agent.py +386 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/agents.py +8 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/gateway.py +3 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_agents.py +41 -7
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_agents.py +45 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_claw.py +134 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_gateway.py +10 -0
- hypercli_sdk-2026.4.6/hypercli/agent.py +0 -215
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/.gitignore +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/README.md +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/client.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/renders.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/voice.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_keys.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.7}/tests/test_renders_subscription.py +0 -0
- {hypercli_sdk-2026.4.6 → 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
|
)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|