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.
Files changed (53) hide show
  1. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/PKG-INFO +1 -1
  2. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/__init__.py +1 -1
  3. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/openclaw/gateway.py +20 -0
  4. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/pyproject.toml +1 -1
  5. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_agents.py +6 -6
  6. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_claw.py +6 -6
  7. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_gateway.py +87 -0
  8. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/.gitignore +0 -0
  9. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/README.md +0 -0
  10. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/agent.py +0 -0
  11. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/agents.py +0 -0
  12. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/billing.py +0 -0
  13. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/client.py +0 -0
  14. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/config.py +0 -0
  15. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/files.py +0 -0
  16. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/gateway.py +0 -0
  17. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/http.py +0 -0
  18. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/instances.py +0 -0
  19. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/__init__.py +0 -0
  20. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/base.py +0 -0
  21. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/comfyui.py +0 -0
  22. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/job/gradio.py +0 -0
  23. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/jobs.py +0 -0
  24. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/keys.py +0 -0
  25. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/logs.py +0 -0
  26. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/models.py +0 -0
  27. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/openclaw/__init__.py +0 -0
  28. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/renders.py +0 -0
  29. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/shell.py +0 -0
  30. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/user.py +0 -0
  31. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/voice.py +0 -0
  32. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/hypercli/x402.py +0 -0
  33. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/conftest.py +0 -0
  34. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_agents.py +0 -0
  35. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_auth.py +0 -0
  36. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_billing.py +0 -0
  37. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_instances.py +0 -0
  38. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_jobs_dryrun.py +0 -0
  39. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_keys.py +0 -0
  40. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/integration/test_renders.py +0 -0
  41. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_apply_params.py +0 -0
  42. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_console_test_key.py +0 -0
  43. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_dev_test_keys.py +0 -0
  44. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_config.py +0 -0
  45. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_exec_shell_dryrun.py +0 -0
  46. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_gateway_retry.py +0 -0
  47. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_graph_to_api.py +0 -0
  48. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_http.py +0 -0
  49. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_jobs.py +0 -0
  50. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_keys.py +0 -0
  51. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_models.py +0 -0
  52. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_renders_subscription.py +0 -0
  53. {hypercli_sdk-2026.4.20 → hypercli_sdk-2026.4.22}/tests/test_voice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 2026.4.20
3
+ Version: 2026.4.22
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
@@ -78,7 +78,7 @@ from .gateway import (
78
78
  extract_gateway_chat_tool_calls,
79
79
  normalize_gateway_chat_message,
80
80
  )
81
- __version__ = "2026.4.20"
81
+ __version__ = "2026.4.22"
82
82
  __all__ = [
83
83
  "HyperCLI",
84
84
  "configure",
@@ -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,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-sdk"
7
- version = "2026.4.20"
7
+ version = "2026.4.22"
8
8
  description = "Python SDK for HyperCLI - GPU orchestration and HyperAgent API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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": "1aiu",
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"] == "1aiu"
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": "1aiu"},
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("1aiu", duration=3600, tags=["customer=acme"])
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/1aiu")
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": "1aiu"},
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": "5aiu",
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 == "5aiu"
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": "1aiu",
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("1aiu", quantity=1, bundle={"small": 1})
412
+ result = agent.purchase_via_x402("basic", quantity=1, bundle={"small": 1})
413
413
 
414
- assert result.plan_id == "1aiu"
415
- assert mock_http._session.post.call_args[0][0] == "https://api.hypercli.com/agents/x402/1aiu"
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 = {}