hypercli-sdk 0.7.0__tar.gz → 0.8.0__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-0.7.0 → hypercli_sdk-0.8.0}/PKG-INFO +4 -1
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/__init__.py +6 -0
- hypercli_sdk-0.8.0/hypercli/claw.py +303 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/client.py +5 -1
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/jobs.py +4 -0
- hypercli_sdk-0.8.0/hypercli/keys.py +62 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/pyproject.toml +5 -1
- hypercli_sdk-0.8.0/tests/test_claw.py +153 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/.gitignore +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/README.md +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/billing.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/config.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/files.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/http.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/instances.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/base.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/logs.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/renders.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/user.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/tests/test_graph_to_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Python SDK for HyperCLI - GPU orchestration and LLM API
|
|
5
5
|
Project-URL: Homepage, https://hypercli.com
|
|
6
6
|
Project-URL: Documentation, https://docs.hypercli.com
|
|
@@ -16,10 +16,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Requires-Dist: httpx>=0.28.1
|
|
18
18
|
Requires-Dist: websockets>=15.0.1
|
|
19
|
+
Provides-Extra: claw
|
|
20
|
+
Requires-Dist: openai>=1.0.0; extra == 'claw'
|
|
19
21
|
Provides-Extra: comfyui
|
|
20
22
|
Requires-Dist: comfyui-workflow-templates-media-image>=0.3.0; extra == 'comfyui'
|
|
21
23
|
Requires-Dist: comfyui-workflow-templates>=0.7.0; extra == 'comfyui'
|
|
22
24
|
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: openai>=1.0.0; extra == 'dev'
|
|
23
26
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
24
27
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
28
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
@@ -8,6 +8,7 @@ from .renders import Render, RenderStatus
|
|
|
8
8
|
from .files import File, AsyncFiles
|
|
9
9
|
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
|
|
10
10
|
from .logs import LogStream, stream_logs, fetch_logs
|
|
11
|
+
from .claw import Claw, ClawKey, ClawPlan, ClawModel
|
|
11
12
|
|
|
12
13
|
__version__ = "0.5.0"
|
|
13
14
|
__all__ = [
|
|
@@ -56,4 +57,9 @@ __all__ = [
|
|
|
56
57
|
"LogStream",
|
|
57
58
|
"stream_logs",
|
|
58
59
|
"fetch_logs",
|
|
60
|
+
# HyperClaw
|
|
61
|
+
"Claw",
|
|
62
|
+
"ClawKey",
|
|
63
|
+
"ClawPlan",
|
|
64
|
+
"ClawModel",
|
|
59
65
|
]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperClaw 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 typing import Optional, List, Dict, Any, Union
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from .http import HTTPClient
|
|
11
|
+
|
|
12
|
+
# OpenAI client is optional - only needed for chat
|
|
13
|
+
try:
|
|
14
|
+
from openai import OpenAI
|
|
15
|
+
OPENAI_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
OPENAI_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ClawKey:
|
|
22
|
+
"""HyperClaw API key with subscription details."""
|
|
23
|
+
key: str
|
|
24
|
+
plan_id: str
|
|
25
|
+
expires_at: datetime
|
|
26
|
+
tpm_limit: int
|
|
27
|
+
rpm_limit: int
|
|
28
|
+
user_id: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data: dict) -> "ClawKey":
|
|
32
|
+
expires = data.get("expires_at", "")
|
|
33
|
+
if isinstance(expires, str):
|
|
34
|
+
expires = datetime.fromisoformat(expires.replace("Z", "+00:00"))
|
|
35
|
+
return cls(
|
|
36
|
+
key=data["key"],
|
|
37
|
+
plan_id=data["plan_id"],
|
|
38
|
+
expires_at=expires,
|
|
39
|
+
tpm_limit=data.get("tpm_limit", 0),
|
|
40
|
+
rpm_limit=data.get("rpm_limit", 0),
|
|
41
|
+
user_id=data.get("user_id"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ClawPlan:
|
|
47
|
+
"""HyperClaw subscription plan."""
|
|
48
|
+
id: str
|
|
49
|
+
name: str
|
|
50
|
+
price_usd: float
|
|
51
|
+
tpm_limit: int
|
|
52
|
+
rpm_limit: int
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: dict) -> "ClawPlan":
|
|
56
|
+
return cls(
|
|
57
|
+
id=data["id"],
|
|
58
|
+
name=data["name"],
|
|
59
|
+
price_usd=data["price_usd"],
|
|
60
|
+
tpm_limit=data["tpm_limit"],
|
|
61
|
+
rpm_limit=data["rpm_limit"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ClawModel:
|
|
67
|
+
"""Available model on HyperClaw."""
|
|
68
|
+
id: str
|
|
69
|
+
name: str
|
|
70
|
+
context_length: int
|
|
71
|
+
supports_vision: bool = False
|
|
72
|
+
supports_function_calling: bool = False
|
|
73
|
+
supports_tool_choice: bool = False
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict) -> "ClawModel":
|
|
77
|
+
caps = data.get("capabilities", {})
|
|
78
|
+
return cls(
|
|
79
|
+
id=data["id"],
|
|
80
|
+
name=data.get("name", data["id"]),
|
|
81
|
+
context_length=data.get("context_length", 0),
|
|
82
|
+
supports_vision=caps.get("supports_vision", False),
|
|
83
|
+
supports_function_calling=caps.get("supports_function_calling", False),
|
|
84
|
+
supports_tool_choice=caps.get("supports_tool_choice", False),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Claw:
|
|
89
|
+
"""
|
|
90
|
+
HyperClaw API Client
|
|
91
|
+
|
|
92
|
+
Provides access to HyperClaw inference endpoints using the OpenAI Python client.
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
from hypercli import HyperCLI
|
|
96
|
+
|
|
97
|
+
client = HyperCLI(claw_api_key="sk-...")
|
|
98
|
+
|
|
99
|
+
# Get OpenAI client for chat
|
|
100
|
+
openai = client.claw.openai
|
|
101
|
+
response = openai.chat.completions.create(
|
|
102
|
+
model="kimi-k2.5",
|
|
103
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Or use the convenience wrapper
|
|
107
|
+
response = client.claw.chat(
|
|
108
|
+
model="kimi-k2.5",
|
|
109
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Vision
|
|
113
|
+
response = client.claw.chat(
|
|
114
|
+
model="kimi-k2.5",
|
|
115
|
+
messages=[{
|
|
116
|
+
"role": "user",
|
|
117
|
+
"content": [
|
|
118
|
+
{"type": "text", "text": "What's in this image?"},
|
|
119
|
+
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
|
|
120
|
+
]
|
|
121
|
+
}]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Function calling
|
|
125
|
+
response = client.claw.chat(
|
|
126
|
+
model="kimi-k2.5",
|
|
127
|
+
messages=[...],
|
|
128
|
+
tools=[{"type": "function", "function": {...}}]
|
|
129
|
+
)
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# Default HyperClaw API base URL
|
|
133
|
+
CLAW_API_BASE = "https://api.hyperclaw.app/v1"
|
|
134
|
+
DEV_API_BASE = "https://dev-api.hyperclaw.app/v1"
|
|
135
|
+
|
|
136
|
+
def __init__(self, http: HTTPClient, claw_api_key: str = None, dev: bool = False):
|
|
137
|
+
"""
|
|
138
|
+
Initialize HyperClaw client.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
http: HTTPClient for making requests (used for non-OpenAI endpoints)
|
|
142
|
+
claw_api_key: Optional separate API key for HyperClaw
|
|
143
|
+
dev: Use dev API endpoint
|
|
144
|
+
"""
|
|
145
|
+
self._http = http
|
|
146
|
+
self._api_key = claw_api_key or http.api_key
|
|
147
|
+
self._dev = dev
|
|
148
|
+
self._base_url = self.DEV_API_BASE if dev else self.CLAW_API_BASE
|
|
149
|
+
self._openai = None
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def openai(self) -> "OpenAI":
|
|
153
|
+
"""
|
|
154
|
+
Get OpenAI client configured for HyperClaw.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
OpenAI client instance
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ImportError: If openai package is not installed
|
|
161
|
+
"""
|
|
162
|
+
if not OPENAI_AVAILABLE:
|
|
163
|
+
raise ImportError(
|
|
164
|
+
"OpenAI package required for chat. Install with: pip install openai"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if self._openai is None:
|
|
168
|
+
self._openai = OpenAI(
|
|
169
|
+
api_key=self._api_key,
|
|
170
|
+
base_url=self._base_url,
|
|
171
|
+
)
|
|
172
|
+
return self._openai
|
|
173
|
+
|
|
174
|
+
def chat(
|
|
175
|
+
self,
|
|
176
|
+
model: str,
|
|
177
|
+
messages: List[Dict],
|
|
178
|
+
temperature: float = None,
|
|
179
|
+
max_tokens: int = None,
|
|
180
|
+
tools: List[Dict] = None,
|
|
181
|
+
tool_choice: Union[str, Dict] = None,
|
|
182
|
+
stream: bool = False,
|
|
183
|
+
**kwargs
|
|
184
|
+
):
|
|
185
|
+
"""
|
|
186
|
+
Create a chat completion using the OpenAI client.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
model: Model ID (e.g., "kimi-k2.5")
|
|
190
|
+
messages: List of message dicts
|
|
191
|
+
temperature: Sampling temperature (0-2)
|
|
192
|
+
max_tokens: Maximum tokens to generate
|
|
193
|
+
tools: List of tool definitions for function calling
|
|
194
|
+
tool_choice: Tool choice mode ("auto", "none", or specific tool)
|
|
195
|
+
stream: Whether to stream the response
|
|
196
|
+
**kwargs: Additional parameters passed to the API
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
OpenAI ChatCompletion object
|
|
200
|
+
"""
|
|
201
|
+
params = {
|
|
202
|
+
"model": model,
|
|
203
|
+
"messages": messages,
|
|
204
|
+
**kwargs
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if temperature is not None:
|
|
208
|
+
params["temperature"] = temperature
|
|
209
|
+
if max_tokens is not None:
|
|
210
|
+
params["max_tokens"] = max_tokens
|
|
211
|
+
if tools:
|
|
212
|
+
params["tools"] = tools
|
|
213
|
+
if tool_choice:
|
|
214
|
+
params["tool_choice"] = tool_choice
|
|
215
|
+
if stream:
|
|
216
|
+
params["stream"] = stream
|
|
217
|
+
|
|
218
|
+
return self.openai.chat.completions.create(**params)
|
|
219
|
+
|
|
220
|
+
def models(self) -> List[ClawModel]:
|
|
221
|
+
"""
|
|
222
|
+
List available models.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of ClawModel objects
|
|
226
|
+
"""
|
|
227
|
+
response = self.openai.models.list()
|
|
228
|
+
return [
|
|
229
|
+
ClawModel.from_dict({
|
|
230
|
+
"id": m.id,
|
|
231
|
+
"name": getattr(m, "name", m.id),
|
|
232
|
+
"context_length": getattr(m, "context_length", 0),
|
|
233
|
+
"capabilities": getattr(m, "capabilities", {}),
|
|
234
|
+
})
|
|
235
|
+
for m in response.data
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
def _api_base_without_v1(self) -> str:
|
|
239
|
+
"""Get API base URL without /v1 suffix."""
|
|
240
|
+
return self._base_url.replace("/v1", "")
|
|
241
|
+
|
|
242
|
+
def key_status(self) -> ClawKey:
|
|
243
|
+
"""
|
|
244
|
+
Get current API key status and subscription details.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
ClawKey object with subscription info
|
|
248
|
+
"""
|
|
249
|
+
r = self._http._session.get(
|
|
250
|
+
f"{self._api_base_without_v1()}/api/keys/status",
|
|
251
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
252
|
+
)
|
|
253
|
+
r.raise_for_status()
|
|
254
|
+
return ClawKey.from_dict(r.json())
|
|
255
|
+
|
|
256
|
+
def plans(self) -> List[ClawPlan]:
|
|
257
|
+
"""
|
|
258
|
+
List available subscription plans.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of ClawPlan objects
|
|
262
|
+
"""
|
|
263
|
+
r = self._http._session.get(
|
|
264
|
+
f"{self._api_base_without_v1()}/api/plans",
|
|
265
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
266
|
+
)
|
|
267
|
+
r.raise_for_status()
|
|
268
|
+
data = r.json()
|
|
269
|
+
return [ClawPlan.from_dict(p) for p in data.get("plans", [])]
|
|
270
|
+
|
|
271
|
+
def discovery_health(self) -> Dict[str, Any]:
|
|
272
|
+
"""
|
|
273
|
+
Get discovery service health status.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict with hosts_total, hosts_healthy, fallbacks_active
|
|
277
|
+
"""
|
|
278
|
+
r = self._http._session.get(
|
|
279
|
+
f"{self._api_base_without_v1()}/discovery/health",
|
|
280
|
+
)
|
|
281
|
+
r.raise_for_status()
|
|
282
|
+
return r.json()
|
|
283
|
+
|
|
284
|
+
def discovery_config(self, api_key: str = None) -> Dict[str, Any]:
|
|
285
|
+
"""
|
|
286
|
+
Get discovery service configuration (requires API key).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
api_key: Backend API key (not the user's claw key)
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dict with hosts, fallbacks, config
|
|
293
|
+
"""
|
|
294
|
+
headers = {}
|
|
295
|
+
if api_key:
|
|
296
|
+
headers["X-API-KEY"] = api_key
|
|
297
|
+
|
|
298
|
+
r = self._http._session.get(
|
|
299
|
+
f"{self._api_base_without_v1()}/discovery/config",
|
|
300
|
+
headers=headers,
|
|
301
|
+
)
|
|
302
|
+
r.raise_for_status()
|
|
303
|
+
return r.json()
|
|
@@ -7,6 +7,8 @@ from .user import UserAPI
|
|
|
7
7
|
from .instances import Instances
|
|
8
8
|
from .renders import Renders
|
|
9
9
|
from .files import Files
|
|
10
|
+
from .claw import Claw
|
|
11
|
+
from .keys import KeysAPI
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class HyperCLI:
|
|
@@ -36,7 +38,7 @@ class HyperCLI:
|
|
|
36
38
|
user = client.user.get()
|
|
37
39
|
"""
|
|
38
40
|
|
|
39
|
-
def __init__(self, api_key: str = None, api_url: str = None):
|
|
41
|
+
def __init__(self, api_key: str = None, api_url: str = None, claw_api_key: str = None, claw_dev: bool = False):
|
|
40
42
|
self._api_key = api_key or get_api_key()
|
|
41
43
|
if not self._api_key:
|
|
42
44
|
raise ValueError(
|
|
@@ -54,6 +56,8 @@ class HyperCLI:
|
|
|
54
56
|
self.instances = Instances(self._http)
|
|
55
57
|
self.renders = Renders(self._http)
|
|
56
58
|
self.files = Files(self._http)
|
|
59
|
+
self.keys = KeysAPI(self._http)
|
|
60
|
+
self.claw = Claw(self._http, claw_api_key=claw_api_key, dev=claw_dev)
|
|
57
61
|
|
|
58
62
|
@property
|
|
59
63
|
def api_url(self) -> str:
|
|
@@ -133,6 +133,7 @@ class Jobs:
|
|
|
133
133
|
env: dict[str, str] = None,
|
|
134
134
|
ports: dict[str, int] = None,
|
|
135
135
|
auth: bool = False,
|
|
136
|
+
registry_auth: dict[str, str] = None,
|
|
136
137
|
) -> Job:
|
|
137
138
|
"""Create a new job.
|
|
138
139
|
|
|
@@ -147,6 +148,7 @@ class Jobs:
|
|
|
147
148
|
env: Environment variables
|
|
148
149
|
ports: Ports to expose. Use {"lb": port} for HTTPS load balancer
|
|
149
150
|
auth: Enable Bearer token auth on load balancer (use with ports={"lb": port})
|
|
151
|
+
registry_auth: Private registry credentials {"username": "...", "password": "..."}
|
|
150
152
|
"""
|
|
151
153
|
payload = {
|
|
152
154
|
"docker_image": image,
|
|
@@ -165,6 +167,8 @@ class Jobs:
|
|
|
165
167
|
payload["ports"] = ports
|
|
166
168
|
if auth:
|
|
167
169
|
payload["auth"] = auth
|
|
170
|
+
if registry_auth:
|
|
171
|
+
payload["registry_auth"] = registry_auth
|
|
168
172
|
|
|
169
173
|
data = self._http.post("/api/jobs", json=payload)
|
|
170
174
|
return Job.from_dict(data)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""API Keys management"""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .http import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ApiKey:
|
|
11
|
+
key_id: str
|
|
12
|
+
name: str
|
|
13
|
+
api_key: Optional[str] # Full key only on create
|
|
14
|
+
api_key_preview: Optional[str] # Masked key on list
|
|
15
|
+
last4: Optional[str]
|
|
16
|
+
is_active: bool
|
|
17
|
+
created_at: str
|
|
18
|
+
last_used_at: Optional[str]
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, data: dict) -> "ApiKey":
|
|
22
|
+
return cls(
|
|
23
|
+
key_id=data.get("key_id", ""),
|
|
24
|
+
name=data.get("name", ""),
|
|
25
|
+
api_key=data.get("api_key"),
|
|
26
|
+
api_key_preview=data.get("api_key_preview"),
|
|
27
|
+
last4=data.get("last4"),
|
|
28
|
+
is_active=data.get("is_active", True),
|
|
29
|
+
created_at=data.get("created_at", ""),
|
|
30
|
+
last_used_at=data.get("last_used_at"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class KeysAPI:
|
|
35
|
+
"""API Keys management"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, http: "HTTPClient"):
|
|
38
|
+
self._http = http
|
|
39
|
+
|
|
40
|
+
def create(self, name: str = "default") -> ApiKey:
|
|
41
|
+
"""Create a new API key"""
|
|
42
|
+
data = self._http.post("/api/keys", json={"name": name})
|
|
43
|
+
return ApiKey.from_dict(data)
|
|
44
|
+
|
|
45
|
+
def list(self) -> List[ApiKey]:
|
|
46
|
+
"""List all API keys (masked)"""
|
|
47
|
+
data = self._http.get("/api/keys")
|
|
48
|
+
return [ApiKey.from_dict(k) for k in data]
|
|
49
|
+
|
|
50
|
+
def get(self, key_id: str) -> ApiKey:
|
|
51
|
+
"""Get a specific API key (masked)"""
|
|
52
|
+
data = self._http.get(f"/api/keys/{key_id}")
|
|
53
|
+
return ApiKey.from_dict(data)
|
|
54
|
+
|
|
55
|
+
def disable(self, key_id: str) -> dict:
|
|
56
|
+
"""Deactivate an API key (irreversible)"""
|
|
57
|
+
return self._http.delete(f"/api/keys/{key_id}")
|
|
58
|
+
|
|
59
|
+
def rename(self, key_id: str, name: str) -> ApiKey:
|
|
60
|
+
"""Rename an API key"""
|
|
61
|
+
data = self._http.patch(f"/api/keys/{key_id}", json={"name": name})
|
|
62
|
+
return ApiKey.from_dict(data)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypercli-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Python SDK for HyperCLI - GPU orchestration and LLM API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,9 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
+
claw = [
|
|
30
|
+
"openai>=1.0.0",
|
|
31
|
+
]
|
|
29
32
|
comfyui = [
|
|
30
33
|
"comfyui-workflow-templates>=0.7.0",
|
|
31
34
|
"comfyui-workflow-templates-media-image>=0.3.0",
|
|
@@ -34,6 +37,7 @@ dev = [
|
|
|
34
37
|
"pytest>=8.0.0",
|
|
35
38
|
"pytest-asyncio>=0.23.0",
|
|
36
39
|
"ruff>=0.3.0",
|
|
40
|
+
"openai>=1.0.0",
|
|
37
41
|
]
|
|
38
42
|
|
|
39
43
|
[project.urls]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for HyperClaw SDK client
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
7
|
+
from hypercli.claw import Claw, ClawKey, ClawPlan, ClawModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestClawDataclasses:
|
|
11
|
+
"""Tests for Claw dataclasses."""
|
|
12
|
+
|
|
13
|
+
def test_claw_key_from_dict(self):
|
|
14
|
+
data = {
|
|
15
|
+
"key": "sk-test-123",
|
|
16
|
+
"plan_id": "5aiu",
|
|
17
|
+
"expires_at": "2026-03-07T12:00:00Z",
|
|
18
|
+
"tpm_limit": 250000,
|
|
19
|
+
"rpm_limit": 5000,
|
|
20
|
+
"user_id": "user-123"
|
|
21
|
+
}
|
|
22
|
+
key = ClawKey.from_dict(data)
|
|
23
|
+
assert key.key == "sk-test-123"
|
|
24
|
+
assert key.plan_id == "5aiu"
|
|
25
|
+
assert key.tpm_limit == 250000
|
|
26
|
+
assert key.rpm_limit == 5000
|
|
27
|
+
|
|
28
|
+
def test_claw_plan_from_dict(self):
|
|
29
|
+
data = {
|
|
30
|
+
"id": "5aiu",
|
|
31
|
+
"name": "5 Agents",
|
|
32
|
+
"price_usd": 3.0,
|
|
33
|
+
"tpm_limit": 250000,
|
|
34
|
+
"rpm_limit": 5000
|
|
35
|
+
}
|
|
36
|
+
plan = ClawPlan.from_dict(data)
|
|
37
|
+
assert plan.id == "5aiu"
|
|
38
|
+
assert plan.price_usd == 3.0
|
|
39
|
+
|
|
40
|
+
def test_claw_model_from_dict(self):
|
|
41
|
+
data = {
|
|
42
|
+
"id": "kimi-k2.5",
|
|
43
|
+
"name": "Kimi K2.5",
|
|
44
|
+
"context_length": 262144,
|
|
45
|
+
"capabilities": {
|
|
46
|
+
"supports_vision": True,
|
|
47
|
+
"supports_function_calling": True,
|
|
48
|
+
"supports_tool_choice": True
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
model = ClawModel.from_dict(data)
|
|
52
|
+
assert model.id == "kimi-k2.5"
|
|
53
|
+
assert model.context_length == 262144
|
|
54
|
+
assert model.supports_vision is True
|
|
55
|
+
assert model.supports_function_calling is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestClawClient:
|
|
59
|
+
"""Tests for Claw client methods."""
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def mock_http(self):
|
|
63
|
+
http = Mock()
|
|
64
|
+
http._api_key = "test-key"
|
|
65
|
+
http._session = Mock()
|
|
66
|
+
return http
|
|
67
|
+
|
|
68
|
+
def test_discovery_health(self, mock_http):
|
|
69
|
+
mock_http._session.get.return_value.json.return_value = {
|
|
70
|
+
"status": "ok",
|
|
71
|
+
"hosts_total": 1,
|
|
72
|
+
"hosts_healthy": 0,
|
|
73
|
+
"fallbacks_active": 1
|
|
74
|
+
}
|
|
75
|
+
mock_http._session.get.return_value.raise_for_status = Mock()
|
|
76
|
+
|
|
77
|
+
claw = Claw(mock_http, dev=True)
|
|
78
|
+
result = claw.discovery_health()
|
|
79
|
+
|
|
80
|
+
assert result["status"] == "ok"
|
|
81
|
+
assert result["hosts_total"] == 1
|
|
82
|
+
mock_http._session.get.assert_called_once()
|
|
83
|
+
|
|
84
|
+
def test_openai_client_creation(self, mock_http):
|
|
85
|
+
"""Test that OpenAI client is created with correct config."""
|
|
86
|
+
claw = Claw(mock_http, claw_api_key="sk-test", dev=True)
|
|
87
|
+
|
|
88
|
+
# Access openai property to trigger creation
|
|
89
|
+
with patch('hypercli.claw.OpenAI') as mock_openai:
|
|
90
|
+
mock_openai.return_value = MagicMock()
|
|
91
|
+
client = claw.openai
|
|
92
|
+
|
|
93
|
+
mock_openai.assert_called_once_with(
|
|
94
|
+
api_key="sk-test",
|
|
95
|
+
base_url="https://dev-api.hyperclaw.app/v1",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_chat_uses_openai_client(self, mock_http):
|
|
99
|
+
"""Test that chat method uses OpenAI client."""
|
|
100
|
+
claw = Claw(mock_http, claw_api_key="sk-test", dev=True)
|
|
101
|
+
|
|
102
|
+
with patch('hypercli.claw.OpenAI') as mock_openai:
|
|
103
|
+
mock_client = MagicMock()
|
|
104
|
+
mock_openai.return_value = mock_client
|
|
105
|
+
|
|
106
|
+
claw.chat(
|
|
107
|
+
model="kimi-k2.5",
|
|
108
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
109
|
+
temperature=0.7,
|
|
110
|
+
max_tokens=100
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
mock_client.chat.completions.create.assert_called_once_with(
|
|
114
|
+
model="kimi-k2.5",
|
|
115
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
116
|
+
temperature=0.7,
|
|
117
|
+
max_tokens=100
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestClawIntegration:
|
|
122
|
+
"""Integration tests for Claw client (require running service)."""
|
|
123
|
+
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def claw_client(self):
|
|
126
|
+
"""Create a Claw client for integration tests."""
|
|
127
|
+
api_key = os.getenv("HYPERCLAW_API_KEY")
|
|
128
|
+
if not api_key:
|
|
129
|
+
pytest.skip("HYPERCLAW_API_KEY not set")
|
|
130
|
+
|
|
131
|
+
# Create minimal mock http for standalone claw
|
|
132
|
+
http = Mock()
|
|
133
|
+
http._api_key = api_key
|
|
134
|
+
import requests
|
|
135
|
+
http._session = requests.Session()
|
|
136
|
+
|
|
137
|
+
return Claw(http, claw_api_key=api_key, dev=True)
|
|
138
|
+
|
|
139
|
+
@pytest.mark.integration
|
|
140
|
+
def test_discovery_health_integration(self, claw_client):
|
|
141
|
+
result = claw_client.discovery_health()
|
|
142
|
+
assert "status" in result
|
|
143
|
+
assert result["status"] == "ok"
|
|
144
|
+
|
|
145
|
+
@pytest.mark.integration
|
|
146
|
+
def test_chat_integration(self, claw_client):
|
|
147
|
+
"""Test actual chat completion (requires running service + credits)."""
|
|
148
|
+
response = claw_client.chat(
|
|
149
|
+
model="kimi-k2.5",
|
|
150
|
+
messages=[{"role": "user", "content": "Say 'hello' and nothing else."}],
|
|
151
|
+
max_tokens=10
|
|
152
|
+
)
|
|
153
|
+
assert response.choices[0].message.content is not None
|
|
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
|