hypercli-sdk 2026.4.20__tar.gz → 2026.4.22__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.20 → hypercli_sdk-2026.4.22}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/__init__.py +1 -1
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/openclaw/gateway.py +20 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_agents.py +6 -6
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_claw.py +6 -6
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_gateway.py +87 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/.gitignore +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/README.md +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/agent.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/agents.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/client.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/gateway.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/models.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/openclaw/__init__.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/renders.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/voice.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_agents.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_console_test_key.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_dev_test_keys.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_keys.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_models.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_renders_subscription.py +0 -0
- {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_voice.py +0 -0
|
@@ -300,6 +300,10 @@ def _read_connect_pairing_request_id(details: Any) -> str | None:
|
|
|
300
300
|
return request_id.strip() if isinstance(request_id, str) and request_id.strip() else None
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
def _is_concurrent_pairing_approval_race(exc: Exception) -> bool:
|
|
304
|
+
return "unknown requestid" in str(exc).lower()
|
|
305
|
+
|
|
306
|
+
|
|
303
307
|
def _is_retryable_connect_error(exc: Exception) -> bool:
|
|
304
308
|
status_code = getattr(exc, "status_code", None)
|
|
305
309
|
response = getattr(exc, "response", None)
|
|
@@ -1081,6 +1085,22 @@ class GatewayClient:
|
|
|
1081
1085
|
delay = min(delay * BACKOFF_MULTIPLIER, MAX_RECONNECT_DELAY)
|
|
1082
1086
|
continue
|
|
1083
1087
|
except Exception as approval_error:
|
|
1088
|
+
if _is_concurrent_pairing_approval_race(approval_error):
|
|
1089
|
+
self._update_pairing_state(
|
|
1090
|
+
GatewayPairingState(
|
|
1091
|
+
request_id=request_id,
|
|
1092
|
+
role=OPERATOR_ROLE,
|
|
1093
|
+
gateway_url=self.url,
|
|
1094
|
+
device_id=identity.device_id,
|
|
1095
|
+
status="approved",
|
|
1096
|
+
updated_at_ms=_now_ms(),
|
|
1097
|
+
)
|
|
1098
|
+
)
|
|
1099
|
+
if ws is not None:
|
|
1100
|
+
await ws.close()
|
|
1101
|
+
await asyncio.sleep(delay)
|
|
1102
|
+
delay = min(delay * BACKOFF_MULTIPLIER, MAX_RECONNECT_DELAY)
|
|
1103
|
+
continue
|
|
1084
1104
|
self._update_pairing_state(
|
|
1085
1105
|
GatewayPairingState(
|
|
1086
1106
|
request_id=request_id,
|
|
@@ -955,7 +955,7 @@ def test_agents_budget(agents_client):
|
|
|
955
955
|
mock_response = Mock()
|
|
956
956
|
mock_response.status_code = 200
|
|
957
957
|
mock_response.json.return_value = {
|
|
958
|
-
"plan_id": "
|
|
958
|
+
"plan_id": "basic",
|
|
959
959
|
"budget": {"max_agents": 5, "total_cpu": 20, "total_memory": 80},
|
|
960
960
|
"used": {"agents": 2, "cpu": 8, "memory": 32},
|
|
961
961
|
"available": {"agents": 3, "cpu": 12, "memory": 48},
|
|
@@ -966,7 +966,7 @@ def test_agents_budget(agents_client):
|
|
|
966
966
|
mock_client_class.return_value = mock_client
|
|
967
967
|
|
|
968
968
|
budget = agents_client.budget()
|
|
969
|
-
assert budget["plan_id"] == "
|
|
969
|
+
assert budget["plan_id"] == "basic"
|
|
970
970
|
assert budget["available"]["cpu"] == 12
|
|
971
971
|
|
|
972
972
|
|
|
@@ -1041,17 +1041,17 @@ def test_agents_purchase_entitlement_from_balance(agents_client):
|
|
|
1041
1041
|
mock_response.status_code = 200
|
|
1042
1042
|
mock_response.json.return_value = {
|
|
1043
1043
|
"grant": {"id": "grant-1", "type": "BALANCE", "duration": 3600},
|
|
1044
|
-
"entitlement": {"id": "ent-1", "plan_id": "
|
|
1044
|
+
"entitlement": {"id": "ent-1", "plan_id": "basic"},
|
|
1045
1045
|
}
|
|
1046
1046
|
mock_client.post.return_value = mock_response
|
|
1047
1047
|
mock_client.__enter__.return_value = mock_client
|
|
1048
1048
|
mock_client.__exit__.return_value = False
|
|
1049
1049
|
mock_client_class.return_value = mock_client
|
|
1050
1050
|
|
|
1051
|
-
result = agents_client.purchase_entitlement_from_balance("
|
|
1051
|
+
result = agents_client.purchase_entitlement_from_balance("basic", duration=3600, tags=["customer=acme"])
|
|
1052
1052
|
|
|
1053
1053
|
assert result["grant"]["type"] == "BALANCE"
|
|
1054
|
-
assert mock_client.post.call_args[0][0].endswith("/billing/balance/
|
|
1054
|
+
assert mock_client.post.call_args[0][0].endswith("/billing/balance/basic")
|
|
1055
1055
|
assert mock_client.post.call_args[1]["json"] == {"duration": 3600, "tags": ["customer=acme"]}
|
|
1056
1056
|
|
|
1057
1057
|
|
|
@@ -1062,7 +1062,7 @@ def test_agents_redeem_grant_code(agents_client):
|
|
|
1062
1062
|
mock_response.status_code = 200
|
|
1063
1063
|
mock_response.json.return_value = {
|
|
1064
1064
|
"grant": {"id": "grant-1", "type": "ACTIVATION_CODE", "code": "promo-123"},
|
|
1065
|
-
"entitlement": {"id": "ent-1", "plan_id": "
|
|
1065
|
+
"entitlement": {"id": "ent-1", "plan_id": "basic"},
|
|
1066
1066
|
}
|
|
1067
1067
|
mock_client.post.return_value = mock_response
|
|
1068
1068
|
mock_client.__enter__.return_value = mock_client
|
|
@@ -33,14 +33,14 @@ class TestHyperAgentDataclasses:
|
|
|
33
33
|
|
|
34
34
|
def test_agent_plan_from_dict(self):
|
|
35
35
|
data = {
|
|
36
|
-
"id": "
|
|
36
|
+
"id": "pro",
|
|
37
37
|
"name": "5 Agents",
|
|
38
38
|
"price_usd": 3.0,
|
|
39
39
|
"tpm_limit": 250000,
|
|
40
40
|
"rpm_limit": 5000
|
|
41
41
|
}
|
|
42
42
|
plan = HyperAgentPlan.from_dict(data)
|
|
43
|
-
assert plan.id == "
|
|
43
|
+
assert plan.id == "pro"
|
|
44
44
|
assert plan.price_usd == 3.0
|
|
45
45
|
|
|
46
46
|
def test_agent_model_from_dict(self):
|
|
@@ -398,7 +398,7 @@ class TestHyperAgentClient:
|
|
|
398
398
|
mock_response.json.return_value = {
|
|
399
399
|
"ok": True,
|
|
400
400
|
"key": "hyper_api_x402",
|
|
401
|
-
"plan_id": "
|
|
401
|
+
"plan_id": "basic",
|
|
402
402
|
"quantity": 1,
|
|
403
403
|
"bundle": {"small": 1},
|
|
404
404
|
"amount_paid": "20.00",
|
|
@@ -409,10 +409,10 @@ class TestHyperAgentClient:
|
|
|
409
409
|
}
|
|
410
410
|
mock_http._session.post.return_value = mock_response
|
|
411
411
|
|
|
412
|
-
result = agent.purchase_via_x402("
|
|
412
|
+
result = agent.purchase_via_x402("basic", quantity=1, bundle={"small": 1})
|
|
413
413
|
|
|
414
|
-
assert result.plan_id == "
|
|
415
|
-
assert mock_http._session.post.call_args[0][0] == "https://api.hypercli.com/agents/x402/
|
|
414
|
+
assert result.plan_id == "basic"
|
|
415
|
+
assert mock_http._session.post.call_args[0][0] == "https://api.hypercli.com/agents/x402/basic"
|
|
416
416
|
assert mock_http._session.post.call_args[1]["json"] == {"quantity": 1, "bundle": {"small": 1}}
|
|
417
417
|
|
|
418
418
|
def test_purchase_bundle_via_x402_uses_bundle_route(self, mock_http):
|
|
@@ -122,6 +122,93 @@ async def test_connect_auto_approves_pairing_and_reconnects(monkeypatch: pytest.
|
|
|
122
122
|
await client.close()
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_connect_treats_unknown_request_id_as_concurrent_pairing_approval(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
127
|
+
sockets: list[MockConnection] = []
|
|
128
|
+
|
|
129
|
+
async def fake_connect(*args, **kwargs):
|
|
130
|
+
conn = MockConnection()
|
|
131
|
+
sockets.append(conn)
|
|
132
|
+
return conn
|
|
133
|
+
|
|
134
|
+
approvals: list[tuple[str, str]] = []
|
|
135
|
+
|
|
136
|
+
async def fake_approve(self: GatewayClient, request_id: str) -> None:
|
|
137
|
+
approvals.append((self.deployment_id or "", request_id))
|
|
138
|
+
raise RuntimeError("unknown requestId")
|
|
139
|
+
|
|
140
|
+
monkeypatch.setattr("hypercli.openclaw.gateway.websockets.connect", fake_connect)
|
|
141
|
+
monkeypatch.setattr(GatewayClient, "_approve_pairing_request", fake_approve)
|
|
142
|
+
|
|
143
|
+
client = GatewayClient(
|
|
144
|
+
url="wss://openclaw-agent.example",
|
|
145
|
+
token="jwt-token",
|
|
146
|
+
gateway_token="gw-token",
|
|
147
|
+
deployment_id="deployment-123",
|
|
148
|
+
api_key="agent-key",
|
|
149
|
+
api_base="https://api.dev.hypercli.com/agents",
|
|
150
|
+
auto_approve_pairing=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
connect_task = asyncio.create_task(client.connect())
|
|
154
|
+
while not sockets:
|
|
155
|
+
await asyncio.sleep(0)
|
|
156
|
+
|
|
157
|
+
first = sockets[0]
|
|
158
|
+
first.push({"type": "event", "event": "connect.challenge", "payload": {"nonce": "nonce-1"}})
|
|
159
|
+
while not first.sent:
|
|
160
|
+
await asyncio.sleep(0)
|
|
161
|
+
connect_request = first.sent[0]
|
|
162
|
+
first.push(
|
|
163
|
+
{
|
|
164
|
+
"type": "res",
|
|
165
|
+
"id": connect_request["id"],
|
|
166
|
+
"ok": False,
|
|
167
|
+
"error": {
|
|
168
|
+
"code": "INVALID_REQUEST",
|
|
169
|
+
"message": "pairing required",
|
|
170
|
+
"details": {
|
|
171
|
+
"code": "PAIRING_REQUIRED",
|
|
172
|
+
"requestId": "pairing-req-race",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
while len(sockets) < 2:
|
|
179
|
+
await asyncio.sleep(0.05)
|
|
180
|
+
|
|
181
|
+
second = sockets[1]
|
|
182
|
+
second.push({"type": "event", "event": "connect.challenge", "payload": {"nonce": "nonce-2"}})
|
|
183
|
+
while not second.sent:
|
|
184
|
+
await asyncio.sleep(0)
|
|
185
|
+
reconnect_request = second.sent[0]
|
|
186
|
+
second.push(
|
|
187
|
+
{
|
|
188
|
+
"type": "res",
|
|
189
|
+
"id": reconnect_request["id"],
|
|
190
|
+
"ok": True,
|
|
191
|
+
"payload": {
|
|
192
|
+
"protocol": 3,
|
|
193
|
+
"server": {"version": "test"},
|
|
194
|
+
"auth": {
|
|
195
|
+
"deviceToken": "device-token-race",
|
|
196
|
+
"role": "operator",
|
|
197
|
+
"scopes": ["operator.admin"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
await connect_task
|
|
204
|
+
|
|
205
|
+
assert approvals == [("deployment-123", "pairing-req-race")]
|
|
206
|
+
assert client.is_connected is True
|
|
207
|
+
assert client.pending_pairing is None
|
|
208
|
+
|
|
209
|
+
await client.close()
|
|
210
|
+
|
|
211
|
+
|
|
125
212
|
@pytest.mark.asyncio
|
|
126
213
|
async def test_approve_pairing_request_uses_direct_local_pairing_api(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
127
214
|
captured: dict = {}
|
|
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
|
|
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
|