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.
Files changed (24) hide show
  1. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/PKG-INFO +4 -1
  2. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/__init__.py +6 -0
  3. hypercli_sdk-0.8.0/hypercli/claw.py +303 -0
  4. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/client.py +5 -1
  5. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/jobs.py +4 -0
  6. hypercli_sdk-0.8.0/hypercli/keys.py +62 -0
  7. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/pyproject.toml +5 -1
  8. hypercli_sdk-0.8.0/tests/test_claw.py +153 -0
  9. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/.gitignore +0 -0
  10. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/README.md +0 -0
  11. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/billing.py +0 -0
  12. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/config.py +0 -0
  13. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/files.py +0 -0
  14. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/http.py +0 -0
  15. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/instances.py +0 -0
  16. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/__init__.py +0 -0
  17. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/base.py +0 -0
  18. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/comfyui.py +0 -0
  19. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/job/gradio.py +0 -0
  20. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/logs.py +0 -0
  21. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/renders.py +0 -0
  22. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/hypercli/user.py +0 -0
  23. {hypercli_sdk-0.7.0 → hypercli_sdk-0.8.0}/tests/test_apply_params.py +0 -0
  24. {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.7.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.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