hypercli-sdk 2026.4.6__tar.gz → 2026.4.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/__init__.py +15 -2
- hypercli_sdk-2026.4.9/hypercli/agent.py +386 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/agents.py +71 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/client.py +2 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/gateway.py +3 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/http.py +2 -2
- hypercli_sdk-2026.4.9/hypercli/models.py +33 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/renders.py +67 -4
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/voice.py +8 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_agents.py +39 -7
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_keys.py +13 -2
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_agents.py +116 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_claw.py +134 -1
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_gateway.py +10 -0
- hypercli_sdk-2026.4.9/tests/test_models.py +27 -0
- hypercli_sdk-2026.4.9/tests/test_renders_subscription.py +188 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_voice.py +6 -3
- hypercli_sdk-2026.4.6/hypercli/agent.py +0 -215
- hypercli_sdk-2026.4.6/tests/test_renders_subscription.py +0 -86
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/.gitignore +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/README.md +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.6 → hypercli_sdk-2026.4.9}/tests/test_keys.py +0 -0
|
@@ -32,13 +32,21 @@ from .jobs import (
|
|
|
32
32
|
)
|
|
33
33
|
from .renders import Render, RenderStatus
|
|
34
34
|
from .voice import VoiceAPI
|
|
35
|
+
from .models import Model, ModelsAPI
|
|
35
36
|
from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, FlowCatalogItem
|
|
36
37
|
from .files import File, AsyncFiles
|
|
37
38
|
from .job import BaseJob, ComfyUIJob, GradioJob, apply_params, apply_graph_modes, find_node, find_nodes, load_template, graph_to_api, expand_subgraphs, DEFAULT_OBJECT_INFO
|
|
38
39
|
from .logs import LogStream, stream_logs, fetch_logs
|
|
39
40
|
from .agents import Deployments, Agent, OpenClawAgent, ExecResult, build_openclaw_routes
|
|
40
41
|
from .shell import ShellSession, shell_connect
|
|
41
|
-
from .agent import
|
|
42
|
+
from .agent import (
|
|
43
|
+
HyperAgent,
|
|
44
|
+
HyperAgentPlan,
|
|
45
|
+
HyperAgentCurrentPlan,
|
|
46
|
+
HyperAgentSubscription,
|
|
47
|
+
HyperAgentSubscriptionSummary,
|
|
48
|
+
HyperAgentModel,
|
|
49
|
+
)
|
|
42
50
|
from .gateway import (
|
|
43
51
|
GatewayClient,
|
|
44
52
|
GatewayError,
|
|
@@ -50,7 +58,7 @@ from .gateway import (
|
|
|
50
58
|
extract_gateway_chat_tool_calls,
|
|
51
59
|
normalize_gateway_chat_message,
|
|
52
60
|
)
|
|
53
|
-
__version__ = "2026.4.
|
|
61
|
+
__version__ = "2026.4.7"
|
|
54
62
|
__all__ = [
|
|
55
63
|
"HyperCLI",
|
|
56
64
|
"configure",
|
|
@@ -86,6 +94,8 @@ __all__ = [
|
|
|
86
94
|
"Render",
|
|
87
95
|
"RenderStatus",
|
|
88
96
|
"VoiceAPI",
|
|
97
|
+
"Model",
|
|
98
|
+
"ModelsAPI",
|
|
89
99
|
# x402 API
|
|
90
100
|
"X402Client",
|
|
91
101
|
"X402JobLaunch",
|
|
@@ -130,6 +140,9 @@ __all__ = [
|
|
|
130
140
|
# HyperAgent
|
|
131
141
|
"HyperAgent",
|
|
132
142
|
"HyperAgentPlan",
|
|
143
|
+
"HyperAgentCurrentPlan",
|
|
144
|
+
"HyperAgentSubscription",
|
|
145
|
+
"HyperAgentSubscriptionSummary",
|
|
133
146
|
"HyperAgentModel",
|
|
134
147
|
# OpenClaw Gateway
|
|
135
148
|
"GatewayClient",
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperAgent API client
|
|
3
|
+
|
|
4
|
+
Provides access to the HyperClaw inference API for AI agents.
|
|
5
|
+
Uses the official OpenAI Python client for chat completions.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
|
|
12
|
+
from .config import get_agents_api_base_url
|
|
13
|
+
from .http import HTTPClient
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from openai import OpenAI
|
|
17
|
+
|
|
18
|
+
OPENAI_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
OpenAI = None
|
|
21
|
+
OPENAI_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class HyperAgentPlan:
|
|
26
|
+
"""HyperAgent subscription plan."""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
price_usd: float
|
|
31
|
+
tpm_limit: int
|
|
32
|
+
rpm_limit: int
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: dict) -> "HyperAgentPlan":
|
|
36
|
+
price = data.get("price_usd", data.get("price", 0))
|
|
37
|
+
return cls(
|
|
38
|
+
id=data["id"],
|
|
39
|
+
name=data.get("name", data["id"]),
|
|
40
|
+
price_usd=float(price or 0),
|
|
41
|
+
tpm_limit=int(data.get("tpm_limit", 0)),
|
|
42
|
+
rpm_limit=int(data.get("rpm_limit", 0)),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class HyperAgentCurrentPlan:
|
|
48
|
+
"""Effective current plan snapshot for an authenticated HyperClaw user."""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
name: str
|
|
52
|
+
price: float | str
|
|
53
|
+
aiu: int | None = None
|
|
54
|
+
agents: int | None = None
|
|
55
|
+
tpm_limit: int = 0
|
|
56
|
+
rpm_limit: int = 0
|
|
57
|
+
expires_at: datetime | None = None
|
|
58
|
+
cancel_at_period_end: bool = False
|
|
59
|
+
provider: str | None = None
|
|
60
|
+
seconds_remaining: int | None = None
|
|
61
|
+
pooled_tpd: int = 0
|
|
62
|
+
slot_inventory: dict[str, Any] | None = None
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict) -> "HyperAgentCurrentPlan":
|
|
66
|
+
expires_at = data.get("expires_at")
|
|
67
|
+
if expires_at:
|
|
68
|
+
expires_at = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
|
|
69
|
+
return cls(
|
|
70
|
+
id=data["id"],
|
|
71
|
+
name=data.get("name", data["id"]),
|
|
72
|
+
price=data.get("price", 0),
|
|
73
|
+
aiu=data.get("aiu"),
|
|
74
|
+
agents=data.get("agents"),
|
|
75
|
+
tpm_limit=int(data.get("tpm_limit", 0) or 0),
|
|
76
|
+
rpm_limit=int(data.get("rpm_limit", 0) or 0),
|
|
77
|
+
expires_at=expires_at,
|
|
78
|
+
cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
|
|
79
|
+
provider=data.get("provider"),
|
|
80
|
+
seconds_remaining=data.get("seconds_remaining"),
|
|
81
|
+
pooled_tpd=int(data.get("pooled_tpd", 0) or 0),
|
|
82
|
+
slot_inventory=data.get("slot_inventory") or None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class HyperAgentSubscription:
|
|
88
|
+
"""A purchased HyperClaw entitlement/subscription instance."""
|
|
89
|
+
|
|
90
|
+
id: str
|
|
91
|
+
user_id: str
|
|
92
|
+
plan_id: str
|
|
93
|
+
plan_name: str
|
|
94
|
+
provider: str
|
|
95
|
+
status: str
|
|
96
|
+
quantity: int = 1
|
|
97
|
+
expires_at: datetime | None = None
|
|
98
|
+
updated_at: datetime | None = None
|
|
99
|
+
stripe_subscription_id: str | None = None
|
|
100
|
+
cancel_at_period_end: bool = False
|
|
101
|
+
can_cancel: bool = False
|
|
102
|
+
is_current: bool = False
|
|
103
|
+
meta: dict[str, Any] | None = None
|
|
104
|
+
plan_tpm_limit: int = 0
|
|
105
|
+
plan_rpm_limit: int = 0
|
|
106
|
+
plan_tpd: int = 0
|
|
107
|
+
plan_agent_tier: str | None = None
|
|
108
|
+
slot_grants: dict[str, int] | None = None
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict(cls, data: dict) -> "HyperAgentSubscription":
|
|
112
|
+
expires_at = data.get("expires_at")
|
|
113
|
+
updated_at = data.get("updated_at")
|
|
114
|
+
return cls(
|
|
115
|
+
id=data["id"],
|
|
116
|
+
user_id=data.get("user_id", ""),
|
|
117
|
+
plan_id=data.get("plan_id", ""),
|
|
118
|
+
plan_name=data.get("plan_name", data.get("plan_id", "")),
|
|
119
|
+
provider=data.get("provider", ""),
|
|
120
|
+
status=data.get("status", ""),
|
|
121
|
+
quantity=int(data.get("quantity", 1) or 1),
|
|
122
|
+
expires_at=datetime.fromisoformat(str(expires_at).replace("Z", "+00:00")) if expires_at else None,
|
|
123
|
+
updated_at=datetime.fromisoformat(str(updated_at).replace("Z", "+00:00")) if updated_at else None,
|
|
124
|
+
stripe_subscription_id=data.get("stripe_subscription_id"),
|
|
125
|
+
cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
|
|
126
|
+
can_cancel=bool(data.get("can_cancel", False)),
|
|
127
|
+
is_current=bool(data.get("is_current", False)),
|
|
128
|
+
meta=data.get("meta") or None,
|
|
129
|
+
plan_tpm_limit=int(data.get("plan_tpm_limit", 0) or 0),
|
|
130
|
+
plan_rpm_limit=int(data.get("plan_rpm_limit", 0) or 0),
|
|
131
|
+
plan_tpd=int(data.get("plan_tpd", 0) or 0),
|
|
132
|
+
plan_agent_tier=data.get("plan_agent_tier"),
|
|
133
|
+
slot_grants=data.get("slot_grants") or None,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class HyperAgentSubscriptionSummary:
|
|
139
|
+
"""Effective entitlement summary for an authenticated HyperClaw user."""
|
|
140
|
+
|
|
141
|
+
effective_plan_id: str
|
|
142
|
+
current_subscription_id: str | None
|
|
143
|
+
pooled_tpm_limit: int
|
|
144
|
+
pooled_rpm_limit: int
|
|
145
|
+
pooled_tpd: int
|
|
146
|
+
slot_inventory: dict[str, Any]
|
|
147
|
+
active_subscription_count: int
|
|
148
|
+
active_subscriptions: list[HyperAgentSubscription]
|
|
149
|
+
subscriptions: list[HyperAgentSubscription]
|
|
150
|
+
user: dict[str, Any]
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_dict(cls, data: dict) -> "HyperAgentSubscriptionSummary":
|
|
154
|
+
return cls(
|
|
155
|
+
effective_plan_id=data.get("effective_plan_id", ""),
|
|
156
|
+
current_subscription_id=data.get("current_subscription_id"),
|
|
157
|
+
pooled_tpm_limit=int(data.get("pooled_tpm_limit", 0) or 0),
|
|
158
|
+
pooled_rpm_limit=int(data.get("pooled_rpm_limit", 0) or 0),
|
|
159
|
+
pooled_tpd=int(data.get("pooled_tpd", 0) or 0),
|
|
160
|
+
slot_inventory=data.get("slot_inventory") or {},
|
|
161
|
+
active_subscription_count=int(data.get("active_subscription_count", 0) or 0),
|
|
162
|
+
active_subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("active_subscriptions", [])],
|
|
163
|
+
subscriptions=[HyperAgentSubscription.from_dict(item) for item in data.get("subscriptions", [])],
|
|
164
|
+
user=data.get("user") or {},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class HyperAgentModel:
|
|
170
|
+
"""Available model on HyperAgent."""
|
|
171
|
+
|
|
172
|
+
id: str
|
|
173
|
+
name: str
|
|
174
|
+
context_length: int
|
|
175
|
+
supports_vision: bool = False
|
|
176
|
+
supports_function_calling: bool = False
|
|
177
|
+
supports_tool_choice: bool = False
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_dict(cls, data: dict) -> "HyperAgentModel":
|
|
181
|
+
caps = data.get("capabilities", {})
|
|
182
|
+
return cls(
|
|
183
|
+
id=data["id"],
|
|
184
|
+
name=data.get("name", data["id"]),
|
|
185
|
+
context_length=data.get("context_length", 0),
|
|
186
|
+
supports_vision=caps.get("supports_vision", False),
|
|
187
|
+
supports_function_calling=caps.get("supports_function_calling", False),
|
|
188
|
+
supports_tool_choice=caps.get("supports_tool_choice", False),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class HyperAgent:
|
|
193
|
+
"""
|
|
194
|
+
HyperAgent API client.
|
|
195
|
+
|
|
196
|
+
Provides access to HyperClaw inference endpoints using the OpenAI Python
|
|
197
|
+
client.
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
from hypercli import HyperCLI
|
|
201
|
+
|
|
202
|
+
client = HyperCLI(agent_api_key="sk-...")
|
|
203
|
+
|
|
204
|
+
openai = client.agent.openai
|
|
205
|
+
response = openai.chat.completions.create(
|
|
206
|
+
model="kimi-k2.5",
|
|
207
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
response = client.agent.chat(
|
|
211
|
+
model="kimi-k2.5",
|
|
212
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
213
|
+
)
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
AGENT_API_BASE = "https://api.hypercli.com/v1"
|
|
217
|
+
DEV_API_BASE = "https://api.dev.hypercli.com/v1"
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
http: HTTPClient,
|
|
222
|
+
agent_api_key: str = None,
|
|
223
|
+
dev: bool = False,
|
|
224
|
+
agents_api_base_url: str | None = None,
|
|
225
|
+
):
|
|
226
|
+
self._http = http
|
|
227
|
+
self._api_key = agent_api_key or http.api_key
|
|
228
|
+
self._dev = dev
|
|
229
|
+
self._base_url = self._resolve_base_url(agents_api_base_url, dev)
|
|
230
|
+
self._control_base_url = self._resolve_control_base_url(getattr(http, "base_url", None), agents_api_base_url, dev)
|
|
231
|
+
self._openai = None
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _resolve_base_url(cls, agents_api_base_url: str | None, dev: bool) -> str:
|
|
235
|
+
raw = (agents_api_base_url or "").rstrip("/")
|
|
236
|
+
if not raw:
|
|
237
|
+
fallback = get_agents_api_base_url(dev).rstrip("/")
|
|
238
|
+
return cls._resolve_base_url(fallback, dev)
|
|
239
|
+
parsed = urlsplit(raw if "://" in raw else f"https://{raw}")
|
|
240
|
+
host = parsed.netloc.lower()
|
|
241
|
+
if host in {"api.hypercli.com", "api.hyperclaw.app", "api.agents.hypercli.com"}:
|
|
242
|
+
return "https://api.agents.hypercli.com/v1"
|
|
243
|
+
if host in {"api.dev.hypercli.com", "api.dev.hyperclaw.app", "dev-api.hyperclaw.app", "api.agents.dev.hypercli.com"}:
|
|
244
|
+
return "https://api.agents.dev.hypercli.com/v1"
|
|
245
|
+
if raw.endswith("/api"):
|
|
246
|
+
return f"{raw[:-4]}/v1"
|
|
247
|
+
if raw.endswith("/agents"):
|
|
248
|
+
return f"{raw[:-7]}/v1"
|
|
249
|
+
if raw:
|
|
250
|
+
return f"{raw}/v1"
|
|
251
|
+
return cls.DEV_API_BASE if dev else cls.AGENT_API_BASE
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def _resolve_control_base_url(
|
|
255
|
+
cls,
|
|
256
|
+
product_api_base_url: str | None,
|
|
257
|
+
agents_api_base_url: str | None,
|
|
258
|
+
dev: bool,
|
|
259
|
+
) -> str:
|
|
260
|
+
raw_agents = (agents_api_base_url or "").rstrip("/")
|
|
261
|
+
if not raw_agents:
|
|
262
|
+
fallback = get_agents_api_base_url(dev).rstrip("/")
|
|
263
|
+
return cls._resolve_control_base_url(None, fallback, dev)
|
|
264
|
+
parsed = urlsplit(raw_agents if "://" in raw_agents else f"https://{raw_agents}")
|
|
265
|
+
scheme = parsed.scheme or "https"
|
|
266
|
+
normalized_path = parsed.path.rstrip("/")
|
|
267
|
+
host = parsed.netloc.lower()
|
|
268
|
+
if normalized_path.endswith("/agents"):
|
|
269
|
+
return f"{scheme}://{parsed.netloc}{normalized_path}"
|
|
270
|
+
if host in {"api.hypercli.com", "api.hyperclaw.app", "api.agents.hypercli.com"}:
|
|
271
|
+
return "https://api.hypercli.com/agents"
|
|
272
|
+
if host in {"api.dev.hypercli.com", "api.dev.hyperclaw.app", "dev-api.hyperclaw.app", "api.agents.dev.hypercli.com"}:
|
|
273
|
+
return "https://api.dev.hypercli.com/agents"
|
|
274
|
+
return f"{scheme}://{parsed.netloc}/agents"
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def openai(self) -> "OpenAI":
|
|
278
|
+
if not OPENAI_AVAILABLE:
|
|
279
|
+
raise ImportError(
|
|
280
|
+
"OpenAI package required for chat. Install with: pip install openai"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if self._openai is None:
|
|
284
|
+
self._openai = OpenAI(
|
|
285
|
+
api_key=self._api_key,
|
|
286
|
+
base_url=self._base_url,
|
|
287
|
+
)
|
|
288
|
+
return self._openai
|
|
289
|
+
|
|
290
|
+
def chat(
|
|
291
|
+
self,
|
|
292
|
+
model: str,
|
|
293
|
+
messages: List[Dict],
|
|
294
|
+
temperature: float = None,
|
|
295
|
+
max_tokens: int = None,
|
|
296
|
+
tools: List[Dict] = None,
|
|
297
|
+
tool_choice: Union[str, Dict] = None,
|
|
298
|
+
stream: bool = False,
|
|
299
|
+
**kwargs,
|
|
300
|
+
):
|
|
301
|
+
params = {
|
|
302
|
+
"model": model,
|
|
303
|
+
"messages": messages,
|
|
304
|
+
**kwargs,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if temperature is not None:
|
|
308
|
+
params["temperature"] = temperature
|
|
309
|
+
if max_tokens is not None:
|
|
310
|
+
params["max_tokens"] = max_tokens
|
|
311
|
+
if tools:
|
|
312
|
+
params["tools"] = tools
|
|
313
|
+
if tool_choice:
|
|
314
|
+
params["tool_choice"] = tool_choice
|
|
315
|
+
if stream:
|
|
316
|
+
params["stream"] = stream
|
|
317
|
+
|
|
318
|
+
return self.openai.chat.completions.create(**params)
|
|
319
|
+
|
|
320
|
+
def models(self) -> List[HyperAgentModel]:
|
|
321
|
+
response = self.openai.models.list()
|
|
322
|
+
return [
|
|
323
|
+
HyperAgentModel.from_dict(
|
|
324
|
+
{
|
|
325
|
+
"id": model.id,
|
|
326
|
+
"name": getattr(model, "name", model.id),
|
|
327
|
+
"context_length": getattr(model, "context_length", 0),
|
|
328
|
+
"capabilities": getattr(model, "capabilities", {}),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
for model in response.data
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
def _api_base_without_v1(self) -> str:
|
|
335
|
+
return self._base_url.replace("/v1", "")
|
|
336
|
+
|
|
337
|
+
def plans(self) -> List[HyperAgentPlan]:
|
|
338
|
+
response = self._http._session.get(
|
|
339
|
+
f"{self._control_base_url}/plans",
|
|
340
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
341
|
+
)
|
|
342
|
+
response.raise_for_status()
|
|
343
|
+
data = response.json()
|
|
344
|
+
return [HyperAgentPlan.from_dict(plan) for plan in data.get("plans", [])]
|
|
345
|
+
|
|
346
|
+
def current_plan(self) -> HyperAgentCurrentPlan:
|
|
347
|
+
response = self._http._session.get(
|
|
348
|
+
f"{self._control_base_url}/plans/current",
|
|
349
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
350
|
+
)
|
|
351
|
+
response.raise_for_status()
|
|
352
|
+
return HyperAgentCurrentPlan.from_dict(response.json())
|
|
353
|
+
|
|
354
|
+
def subscriptions(self) -> list[HyperAgentSubscription]:
|
|
355
|
+
response = self._http._session.get(
|
|
356
|
+
f"{self._control_base_url}/subscriptions",
|
|
357
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
358
|
+
)
|
|
359
|
+
response.raise_for_status()
|
|
360
|
+
data = response.json()
|
|
361
|
+
return [HyperAgentSubscription.from_dict(item) for item in data.get("items", [])]
|
|
362
|
+
|
|
363
|
+
def subscription_summary(self) -> HyperAgentSubscriptionSummary:
|
|
364
|
+
response = self._http._session.get(
|
|
365
|
+
f"{self._control_base_url}/subscriptions/summary",
|
|
366
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
367
|
+
)
|
|
368
|
+
response.raise_for_status()
|
|
369
|
+
return HyperAgentSubscriptionSummary.from_dict(response.json())
|
|
370
|
+
|
|
371
|
+
def discovery_health(self) -> Dict[str, Any]:
|
|
372
|
+
response = self._http._session.get(f"{self._api_base_without_v1()}/discovery/health")
|
|
373
|
+
response.raise_for_status()
|
|
374
|
+
return response.json()
|
|
375
|
+
|
|
376
|
+
def discovery_config(self, api_key: str = None) -> Dict[str, Any]:
|
|
377
|
+
headers = {}
|
|
378
|
+
if api_key:
|
|
379
|
+
headers["X-API-KEY"] = api_key
|
|
380
|
+
|
|
381
|
+
response = self._http._session.get(
|
|
382
|
+
f"{self._api_base_without_v1()}/discovery/config",
|
|
383
|
+
headers=headers,
|
|
384
|
+
)
|
|
385
|
+
response.raise_for_status()
|
|
386
|
+
return response.json()
|
|
@@ -258,6 +258,7 @@ def _deep_merge_config(base: dict[str, Any], patch: dict[str, Any]) -> dict[str,
|
|
|
258
258
|
|
|
259
259
|
|
|
260
260
|
def _agent_kwargs_from_dict(data: dict) -> dict[str, Any]:
|
|
261
|
+
meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
|
|
261
262
|
return {
|
|
262
263
|
"id": data.get("id", ""),
|
|
263
264
|
"user_id": data.get("user_id", ""),
|
|
@@ -277,6 +278,7 @@ def _agent_kwargs_from_dict(data: dict) -> dict[str, Any]:
|
|
|
277
278
|
"created_at": _parse_dt(data.get("created_at")),
|
|
278
279
|
"updated_at": _parse_dt(data.get("updated_at")),
|
|
279
280
|
"launch_config": data.get("launch_config"),
|
|
281
|
+
"meta_ui": copy.deepcopy(meta.get("ui")) if isinstance(meta.get("ui"), dict) else None,
|
|
280
282
|
"routes": data.get("routes") or {},
|
|
281
283
|
"command": data.get("command") or [],
|
|
282
284
|
"entrypoint": data.get("entrypoint") or [],
|
|
@@ -306,6 +308,7 @@ class Agent:
|
|
|
306
308
|
created_at: Optional[datetime] = None
|
|
307
309
|
updated_at: Optional[datetime] = None
|
|
308
310
|
launch_config: Optional[dict] = None
|
|
311
|
+
meta_ui: Optional[dict] = None
|
|
309
312
|
routes: dict[str, dict] = field(default_factory=dict)
|
|
310
313
|
command: list[str] = field(default_factory=list)
|
|
311
314
|
entrypoint: list[str] = field(default_factory=list)
|
|
@@ -383,6 +386,32 @@ class Agent:
|
|
|
383
386
|
self._deployments = agent._deployments
|
|
384
387
|
return self
|
|
385
388
|
|
|
389
|
+
def update(
|
|
390
|
+
self,
|
|
391
|
+
*,
|
|
392
|
+
name: str | None = None,
|
|
393
|
+
size: str | None = None,
|
|
394
|
+
cpu: float | None = None,
|
|
395
|
+
memory: int | None = None,
|
|
396
|
+
refresh_from_lagoon: bool | None = None,
|
|
397
|
+
last_error: str | None = None,
|
|
398
|
+
) -> "Agent":
|
|
399
|
+
agent = self._require_deployments().update(
|
|
400
|
+
self.id,
|
|
401
|
+
name=name,
|
|
402
|
+
size=size,
|
|
403
|
+
cpu=cpu,
|
|
404
|
+
memory=memory,
|
|
405
|
+
refresh_from_lagoon=refresh_from_lagoon,
|
|
406
|
+
last_error=last_error,
|
|
407
|
+
)
|
|
408
|
+
self.__dict__.update(agent.__dict__)
|
|
409
|
+
self._deployments = agent._deployments
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
def resize(self, *, size: str | None = None, cpu: float | None = None, memory: int | None = None) -> "Agent":
|
|
413
|
+
return self.update(size=size, cpu=cpu, memory=memory)
|
|
414
|
+
|
|
386
415
|
def env(self) -> dict[str, str]:
|
|
387
416
|
"""Fetch runtime environment from the pod's K8s secret."""
|
|
388
417
|
data = self._require_deployments().env(self.id)
|
|
@@ -992,6 +1021,7 @@ class Deployments:
|
|
|
992
1021
|
registry_url: str = None,
|
|
993
1022
|
registry_auth: dict = None,
|
|
994
1023
|
gateway_token: str = None,
|
|
1024
|
+
meta_ui: dict = None,
|
|
995
1025
|
dry_run: bool = False,
|
|
996
1026
|
start: bool = True,
|
|
997
1027
|
) -> Agent:
|
|
@@ -1035,6 +1065,8 @@ class Deployments:
|
|
|
1035
1065
|
body["cpu"] = cpu
|
|
1036
1066
|
if memory is not None:
|
|
1037
1067
|
body["memory"] = memory
|
|
1068
|
+
if meta_ui:
|
|
1069
|
+
body["meta"] = {"ui": copy.deepcopy(meta_ui)}
|
|
1038
1070
|
if tags:
|
|
1039
1071
|
body["tags"] = list(tags)
|
|
1040
1072
|
data = self._post(AGENTS_API_PREFIX, json=body)
|
|
@@ -1065,6 +1097,7 @@ class Deployments:
|
|
|
1065
1097
|
registry_url: str = None,
|
|
1066
1098
|
registry_auth: dict = None,
|
|
1067
1099
|
gateway_token: str = None,
|
|
1100
|
+
meta_ui: dict = None,
|
|
1068
1101
|
dry_run: bool = False,
|
|
1069
1102
|
start: bool = True,
|
|
1070
1103
|
openclaw_routes: dict | None = None,
|
|
@@ -1092,6 +1125,7 @@ class Deployments:
|
|
|
1092
1125
|
registry_url=registry_url,
|
|
1093
1126
|
registry_auth=registry_auth,
|
|
1094
1127
|
gateway_token=gateway_token,
|
|
1128
|
+
meta_ui=meta_ui,
|
|
1095
1129
|
dry_run=dry_run,
|
|
1096
1130
|
start=start,
|
|
1097
1131
|
)
|
|
@@ -1242,6 +1276,43 @@ class Deployments:
|
|
|
1242
1276
|
dry_run=dry_run,
|
|
1243
1277
|
)
|
|
1244
1278
|
|
|
1279
|
+
def update(
|
|
1280
|
+
self,
|
|
1281
|
+
agent_id: str,
|
|
1282
|
+
*,
|
|
1283
|
+
name: str | None = None,
|
|
1284
|
+
size: str | None = None,
|
|
1285
|
+
cpu: float | None = None,
|
|
1286
|
+
memory: int | None = None,
|
|
1287
|
+
refresh_from_lagoon: bool | None = None,
|
|
1288
|
+
last_error: str | None = None,
|
|
1289
|
+
) -> Agent:
|
|
1290
|
+
body: dict[str, Any] = {}
|
|
1291
|
+
if name is not None:
|
|
1292
|
+
body["name"] = name
|
|
1293
|
+
if size is not None:
|
|
1294
|
+
body["size"] = size
|
|
1295
|
+
if cpu is not None:
|
|
1296
|
+
body["cpu"] = cpu
|
|
1297
|
+
if memory is not None:
|
|
1298
|
+
body["memory"] = memory
|
|
1299
|
+
if refresh_from_lagoon is not None:
|
|
1300
|
+
body["refresh_from_lagoon"] = refresh_from_lagoon
|
|
1301
|
+
if last_error is not None:
|
|
1302
|
+
body["last_error"] = last_error
|
|
1303
|
+
data = self._http.patch(f"{AGENTS_API_PREFIX}/{agent_id}", json=body)
|
|
1304
|
+
return self._hydrate_agent(data)
|
|
1305
|
+
|
|
1306
|
+
def resize(
|
|
1307
|
+
self,
|
|
1308
|
+
agent_id: str,
|
|
1309
|
+
*,
|
|
1310
|
+
size: str | None = None,
|
|
1311
|
+
cpu: float | None = None,
|
|
1312
|
+
memory: int | None = None,
|
|
1313
|
+
) -> Agent:
|
|
1314
|
+
return self.update(agent_id, size=size, cpu=cpu, memory=memory)
|
|
1315
|
+
|
|
1245
1316
|
def stop(self, agent_id: str) -> Agent:
|
|
1246
1317
|
"""Stop an agent (tears down pod, keeps DB record).
|
|
1247
1318
|
|
|
@@ -19,6 +19,7 @@ from .voice import VoiceAPI
|
|
|
19
19
|
from .agents import Deployments
|
|
20
20
|
from .agent import HyperAgent
|
|
21
21
|
from .keys import KeysAPI
|
|
22
|
+
from .models import ModelsAPI
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def _derive_agents_api_base(api_url: str, agent_dev: bool) -> str:
|
|
@@ -100,6 +101,7 @@ class HyperCLI:
|
|
|
100
101
|
self.files = Files(self._http)
|
|
101
102
|
self.voice = VoiceAPI(self._http)
|
|
102
103
|
self.keys = KeysAPI(self._http)
|
|
104
|
+
self.models = ModelsAPI(self._http)
|
|
103
105
|
self.agent = HyperAgent(
|
|
104
106
|
self._http,
|
|
105
107
|
agent_api_key=resolved_agent_api_key,
|
|
@@ -783,6 +783,9 @@ class GatewayClient:
|
|
|
783
783
|
def pending_pairing(self) -> GatewayPairingState | None:
|
|
784
784
|
return self._pending_pairing
|
|
785
785
|
|
|
786
|
+
def set_gateway_token(self, token: str | None) -> None:
|
|
787
|
+
self.gateway_token = token.strip() if isinstance(token, str) and token.strip() else None
|
|
788
|
+
|
|
786
789
|
def _storage_scope(self) -> str:
|
|
787
790
|
return self.deployment_id or self.url
|
|
788
791
|
|
|
@@ -166,10 +166,10 @@ class HTTPClient:
|
|
|
166
166
|
)
|
|
167
167
|
return _handle_response(resp)
|
|
168
168
|
|
|
169
|
-
def post_bytes(self, path: str, json: dict = None) -> bytes:
|
|
169
|
+
def post_bytes(self, path: str, json: dict = None, timeout: float | None = None) -> bytes:
|
|
170
170
|
resp = request_with_retry(
|
|
171
171
|
"post", f"{self.base_url}{path}",
|
|
172
|
-
headers=self.headers, timeout=self.timeout, json=json
|
|
172
|
+
headers=self.headers, timeout=timeout if timeout is not None else self.timeout, json=json
|
|
173
173
|
)
|
|
174
174
|
return _handle_bytes_response(resp)
|
|
175
175
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""OpenAI-compatible models API"""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING, List
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .http import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Model:
|
|
11
|
+
id: str
|
|
12
|
+
object: str
|
|
13
|
+
owned_by: str | None = None
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, data: dict) -> "Model":
|
|
17
|
+
return cls(
|
|
18
|
+
id=data.get("id", ""),
|
|
19
|
+
object=data.get("object", "model"),
|
|
20
|
+
owned_by=data.get("owned_by"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelsAPI:
|
|
25
|
+
"""OpenAI-compatible models API"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, http: "HTTPClient"):
|
|
28
|
+
self._http = http
|
|
29
|
+
|
|
30
|
+
def list(self) -> List[Model]:
|
|
31
|
+
payload = self._http.get("/v1/models")
|
|
32
|
+
data = payload.get("data") if isinstance(payload, dict) else payload
|
|
33
|
+
return [Model.from_dict(item) for item in (data or [])]
|