hypercli-sdk 2026.4.7__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.7 → hypercli_sdk-2026.4.9}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/__init__.py +3 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/agents.py +63 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/client.py +2 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/http.py +2 -2
- hypercli_sdk-2026.4.9/hypercli/models.py +33 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/renders.py +67 -4
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/voice.py +8 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_agents.py +29 -31
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_keys.py +13 -2
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_agents.py +71 -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.7 → hypercli_sdk-2026.4.9}/tests/test_voice.py +6 -3
- hypercli_sdk-2026.4.7/tests/test_renders_subscription.py +0 -86
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/.gitignore +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/README.md +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/agent.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/gateway.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_claw.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_gateway.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.7 → hypercli_sdk-2026.4.9}/tests/test_keys.py +0 -0
|
@@ -32,6 +32,7 @@ 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
|
|
@@ -93,6 +94,8 @@ __all__ = [
|
|
|
93
94
|
"Render",
|
|
94
95
|
"RenderStatus",
|
|
95
96
|
"VoiceAPI",
|
|
97
|
+
"Model",
|
|
98
|
+
"ModelsAPI",
|
|
96
99
|
# x402 API
|
|
97
100
|
"X402Client",
|
|
98
101
|
"X402JobLaunch",
|
|
@@ -386,6 +386,32 @@ class Agent:
|
|
|
386
386
|
self._deployments = agent._deployments
|
|
387
387
|
return self
|
|
388
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
|
+
|
|
389
415
|
def env(self) -> dict[str, str]:
|
|
390
416
|
"""Fetch runtime environment from the pod's K8s secret."""
|
|
391
417
|
data = self._require_deployments().env(self.id)
|
|
@@ -1250,6 +1276,43 @@ class Deployments:
|
|
|
1250
1276
|
dry_run=dry_run,
|
|
1251
1277
|
)
|
|
1252
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
|
+
|
|
1253
1316
|
def stop(self, agent_id: str) -> Agent:
|
|
1254
1317
|
"""Stop an agent (tears down pod, keeps DB record).
|
|
1255
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,
|
|
@@ -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 [])]
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
"""Renders API"""
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import time
|
|
5
7
|
from typing import TYPE_CHECKING, Any, List
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
@@ -56,11 +58,28 @@ class RenderStatus:
|
|
|
56
58
|
class Renders:
|
|
57
59
|
"""Renders API wrapper"""
|
|
58
60
|
|
|
61
|
+
DEFAULT_WAIT_TIMEOUT = 3600.0
|
|
62
|
+
DEFAULT_QUEUE_GRACE = 1800.0
|
|
63
|
+
DEFAULT_ACTIVE_GRACE = 300.0
|
|
64
|
+
|
|
59
65
|
def __init__(self, http: "HTTPClient", auth_http: "HTTPClient" | None = None):
|
|
60
66
|
self._http = http
|
|
61
67
|
self._auth_http = auth_http or http
|
|
62
68
|
self._auth_me_cache: dict[str, Any] | None = None
|
|
63
69
|
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _parse_render_timestamp(value: Any) -> float | None:
|
|
72
|
+
if value is None:
|
|
73
|
+
return None
|
|
74
|
+
if isinstance(value, (int, float)):
|
|
75
|
+
return float(value)
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
try:
|
|
78
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc).timestamp()
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
return None
|
|
82
|
+
|
|
64
83
|
def _auth_me(self) -> dict[str, Any]:
|
|
65
84
|
if self._auth_me_cache is None:
|
|
66
85
|
self._auth_me_cache = self._auth_http.get("/api/auth/me")
|
|
@@ -93,21 +112,21 @@ class Renders:
|
|
|
93
112
|
|
|
94
113
|
def _get_render(self, render_id: str, *, status: bool = False) -> dict[str, Any]:
|
|
95
114
|
suffix = "/status" if status else ""
|
|
96
|
-
primary = f"/agents/flow/renders/{render_id}{suffix}" if self._supports_subscription_family("flows") else f"/api/renders/{render_id}{suffix}"
|
|
115
|
+
primary = f"/agents/flow/renders/{render_id}{suffix}" if self._supports_subscription_family("flows") else f"/api/flow/renders/{render_id}{suffix}"
|
|
97
116
|
try:
|
|
98
117
|
return self._http.get(primary)
|
|
99
118
|
except APIError as exc:
|
|
100
119
|
if primary.startswith("/agents/flow/") and exc.status_code in {403, 404}:
|
|
101
|
-
return self._http.get(f"/api/renders/{render_id}{suffix}")
|
|
120
|
+
return self._http.get(f"/api/flow/renders/{render_id}{suffix}")
|
|
102
121
|
raise
|
|
103
122
|
|
|
104
123
|
def _delete_render(self, render_id: str) -> dict:
|
|
105
|
-
primary = f"/agents/flow/renders/{render_id}" if self._supports_subscription_family("flows") else f"/api/renders/{render_id}"
|
|
124
|
+
primary = f"/agents/flow/renders/{render_id}" if self._supports_subscription_family("flows") else f"/api/flow/renders/{render_id}"
|
|
106
125
|
try:
|
|
107
126
|
return self._http.delete(primary)
|
|
108
127
|
except APIError as exc:
|
|
109
128
|
if primary.startswith("/agents/flow/") and exc.status_code in {403, 404}:
|
|
110
|
-
return self._http.delete(f"/api/renders/{render_id}")
|
|
129
|
+
return self._http.delete(f"/api/flow/renders/{render_id}")
|
|
111
130
|
raise
|
|
112
131
|
|
|
113
132
|
def list(
|
|
@@ -179,6 +198,50 @@ class Renders:
|
|
|
179
198
|
data = self._get_render(render_id, status=True)
|
|
180
199
|
return RenderStatus.from_dict(data)
|
|
181
200
|
|
|
201
|
+
def wait(
|
|
202
|
+
self,
|
|
203
|
+
render_id: str,
|
|
204
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
205
|
+
poll_interval: float = 5.0,
|
|
206
|
+
queue_grace: float = DEFAULT_QUEUE_GRACE,
|
|
207
|
+
active_grace: float = DEFAULT_ACTIVE_GRACE,
|
|
208
|
+
) -> Render:
|
|
209
|
+
"""Wait for a render to reach a terminal state.
|
|
210
|
+
|
|
211
|
+
This tolerates long queue times in shared dev environments by granting
|
|
212
|
+
one bounded queue grace window and one bounded active-runtime grace
|
|
213
|
+
window when the render only starts near the original deadline.
|
|
214
|
+
"""
|
|
215
|
+
deadline = time.time() + timeout
|
|
216
|
+
queue_grace_used = False
|
|
217
|
+
active_grace_used = False
|
|
218
|
+
last_render: Render | None = None
|
|
219
|
+
|
|
220
|
+
while True:
|
|
221
|
+
render = self.get(render_id)
|
|
222
|
+
last_render = render
|
|
223
|
+
state = (render.state or "").lower()
|
|
224
|
+
if state in {"completed", "failed", "cancelled"}:
|
|
225
|
+
return render
|
|
226
|
+
|
|
227
|
+
now = time.time()
|
|
228
|
+
if now >= deadline:
|
|
229
|
+
started_at = self._parse_render_timestamp(render.started_at)
|
|
230
|
+
if not queue_grace_used and started_at is None:
|
|
231
|
+
queue_grace_used = True
|
|
232
|
+
deadline = now + queue_grace
|
|
233
|
+
elif not active_grace_used and started_at is not None:
|
|
234
|
+
active_grace_used = True
|
|
235
|
+
deadline = max(deadline, started_at + active_grace)
|
|
236
|
+
else:
|
|
237
|
+
raise TimeoutError(
|
|
238
|
+
f"Render {render_id} did not complete within {timeout:.0f}s "
|
|
239
|
+
f"(+{queue_grace:.0f}s queue grace, +{active_grace:.0f}s active grace); "
|
|
240
|
+
f"last_render={last_render}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
time.sleep(poll_interval)
|
|
244
|
+
|
|
182
245
|
# =========================================================================
|
|
183
246
|
# Flow endpoints - simplified interfaces
|
|
184
247
|
# =========================================================================
|
|
@@ -20,6 +20,8 @@ def _encode_reference_audio(ref_audio: bytes | str | Path) -> str:
|
|
|
20
20
|
class VoiceAPI:
|
|
21
21
|
"""Voice capability API wrapper."""
|
|
22
22
|
|
|
23
|
+
DEFAULT_TIMEOUT = 300.0
|
|
24
|
+
|
|
23
25
|
def __init__(self, http: "HTTPClient"):
|
|
24
26
|
self._http = http
|
|
25
27
|
|
|
@@ -30,6 +32,7 @@ class VoiceAPI:
|
|
|
30
32
|
voice: str = "Chelsie",
|
|
31
33
|
language: str = "auto",
|
|
32
34
|
response_format: str = "mp3",
|
|
35
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
33
36
|
) -> bytes:
|
|
34
37
|
return self._http.post_bytes(
|
|
35
38
|
"/agents/voice/tts",
|
|
@@ -39,6 +42,7 @@ class VoiceAPI:
|
|
|
39
42
|
"language": language,
|
|
40
43
|
"response_format": response_format,
|
|
41
44
|
},
|
|
45
|
+
timeout=timeout,
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
def clone(
|
|
@@ -49,6 +53,7 @@ class VoiceAPI:
|
|
|
49
53
|
language: str = "auto",
|
|
50
54
|
x_vector_only: bool = True,
|
|
51
55
|
response_format: str = "mp3",
|
|
56
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
52
57
|
) -> bytes:
|
|
53
58
|
return self._http.post_bytes(
|
|
54
59
|
"/agents/voice/clone",
|
|
@@ -59,6 +64,7 @@ class VoiceAPI:
|
|
|
59
64
|
"x_vector_only": x_vector_only,
|
|
60
65
|
"response_format": response_format,
|
|
61
66
|
},
|
|
67
|
+
timeout=timeout,
|
|
62
68
|
)
|
|
63
69
|
|
|
64
70
|
def design(
|
|
@@ -68,6 +74,7 @@ class VoiceAPI:
|
|
|
68
74
|
description: str,
|
|
69
75
|
language: str = "auto",
|
|
70
76
|
response_format: str = "mp3",
|
|
77
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
71
78
|
) -> bytes:
|
|
72
79
|
return self._http.post_bytes(
|
|
73
80
|
"/agents/voice/design",
|
|
@@ -77,4 +84,5 @@ class VoiceAPI:
|
|
|
77
84
|
"language": language,
|
|
78
85
|
"response_format": response_format,
|
|
79
86
|
},
|
|
87
|
+
timeout=timeout,
|
|
80
88
|
)
|
|
@@ -9,37 +9,35 @@ from hypercli.http import APIError
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _create_agent_with_available_tier(client: HyperCLI, name: str, tags: list[str]) -> tuple[str, str]:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
raise last_error
|
|
42
|
-
raise AssertionError("No available entitlement slots for integration agent tests")
|
|
12
|
+
budget = client.deployments.budget()
|
|
13
|
+
slots = (budget or {}).get("slots") or {}
|
|
14
|
+
|
|
15
|
+
tier = next((candidate for candidate in ("large", "medium", "small") if int((slots.get(candidate) or {}).get("available") or 0) > 0), None)
|
|
16
|
+
if not tier:
|
|
17
|
+
raise AssertionError("No available entitlement slots for integration agent tests")
|
|
18
|
+
|
|
19
|
+
agent_id: str | None = None
|
|
20
|
+
try:
|
|
21
|
+
agent = client.deployments.create(
|
|
22
|
+
name=name,
|
|
23
|
+
size=tier,
|
|
24
|
+
start=False,
|
|
25
|
+
tags=tags,
|
|
26
|
+
)
|
|
27
|
+
agent_id = agent.id
|
|
28
|
+
client.deployments.start_openclaw(agent.id, dry_run=True)
|
|
29
|
+
return agent.id, tier
|
|
30
|
+
except APIError as exc:
|
|
31
|
+
if agent_id:
|
|
32
|
+
try:
|
|
33
|
+
client.deployments.delete(agent_id)
|
|
34
|
+
except APIError:
|
|
35
|
+
pass
|
|
36
|
+
if exc.status_code == 429:
|
|
37
|
+
raise AssertionError(
|
|
38
|
+
f"Budget reported '{tier}' available but dry-run start was rejected for slot exhaustion"
|
|
39
|
+
) from exc
|
|
40
|
+
raise
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
def test_list_agents_requires_agent_key(client, test_agent_api_key: str):
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import uuid
|
|
4
2
|
|
|
5
3
|
import pytest
|
|
@@ -40,6 +38,19 @@ def test_create_and_disable_tagged_key(client):
|
|
|
40
38
|
assert disabled["status"] == "deactivated"
|
|
41
39
|
|
|
42
40
|
|
|
41
|
+
def test_created_key_authenticates_against_models_api(client, test_api_base: str):
|
|
42
|
+
name = f"sdk-models-scope-{uuid.uuid4().hex[:8]}"
|
|
43
|
+
created = client.keys.create(name=name, tags=["models:*"])
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
scoped = HyperCLI(api_key=created.api_key, api_url=test_api_base)
|
|
47
|
+
models = scoped.models.list()
|
|
48
|
+
assert models
|
|
49
|
+
assert all(model.id for model in models)
|
|
50
|
+
finally:
|
|
51
|
+
client.keys.disable(created.key_id)
|
|
52
|
+
|
|
53
|
+
|
|
43
54
|
def test_api_scoped_key_allows_key_admin_but_denies_profile(client, test_api_base: str):
|
|
44
55
|
name = f"sdk-api-scope-{uuid.uuid4().hex[:8]}"
|
|
45
56
|
created = client.keys.create(name=name, tags=["api:self"])
|
|
@@ -785,6 +785,77 @@ def test_agents_start_stop_delete(agents_client):
|
|
|
785
785
|
assert agents_client.delete("agent-123") == {"status": "deleted"}
|
|
786
786
|
|
|
787
787
|
|
|
788
|
+
def test_agents_update_and_resize(agents_client):
|
|
789
|
+
patch_calls = []
|
|
790
|
+
|
|
791
|
+
def fake_patch(path, json=None):
|
|
792
|
+
patch_calls.append((path, json))
|
|
793
|
+
return {
|
|
794
|
+
"id": "agent-123",
|
|
795
|
+
"user_id": "user-456",
|
|
796
|
+
"pod_id": None,
|
|
797
|
+
"pod_name": None,
|
|
798
|
+
"state": "stopped",
|
|
799
|
+
"cpu": 4,
|
|
800
|
+
"memory": 4,
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
agents_client._http.patch = fake_patch
|
|
804
|
+
|
|
805
|
+
updated = agents_client.update("agent-123", size="large", refresh_from_lagoon=True)
|
|
806
|
+
assert updated.id == "agent-123"
|
|
807
|
+
assert patch_calls[0] == (
|
|
808
|
+
"/deployments/agent-123",
|
|
809
|
+
{"size": "large", "refresh_from_lagoon": True},
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
resized = agents_client.resize("agent-123", size="large")
|
|
813
|
+
assert resized.id == "agent-123"
|
|
814
|
+
assert patch_calls[1] == ("/deployments/agent-123", {"size": "large"})
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def test_bound_agent_resize_delegates_to_deployments(agents_client):
|
|
818
|
+
patch_calls = []
|
|
819
|
+
|
|
820
|
+
def fake_patch(path, json=None):
|
|
821
|
+
patch_calls.append((path, json))
|
|
822
|
+
return {
|
|
823
|
+
"id": "agent-123",
|
|
824
|
+
"user_id": "user-456",
|
|
825
|
+
"pod_id": None,
|
|
826
|
+
"pod_name": None,
|
|
827
|
+
"state": "stopped",
|
|
828
|
+
"cpu": 4,
|
|
829
|
+
"memory": 4,
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
agents_client._http.patch = fake_patch
|
|
833
|
+
|
|
834
|
+
with patch("httpx.Client") as mock_client_class:
|
|
835
|
+
mock_client = MagicMock()
|
|
836
|
+
get_response = Mock()
|
|
837
|
+
get_response.status_code = 200
|
|
838
|
+
get_response.json.return_value = {
|
|
839
|
+
"id": "agent-123",
|
|
840
|
+
"user_id": "user-456",
|
|
841
|
+
"pod_id": None,
|
|
842
|
+
"pod_name": None,
|
|
843
|
+
"state": "stopped",
|
|
844
|
+
"cpu": 2,
|
|
845
|
+
"memory": 2,
|
|
846
|
+
}
|
|
847
|
+
mock_client.get.return_value = get_response
|
|
848
|
+
mock_client.__enter__.return_value = mock_client
|
|
849
|
+
mock_client.__exit__.return_value = False
|
|
850
|
+
mock_client_class.return_value = mock_client
|
|
851
|
+
|
|
852
|
+
agent = agents_client.get("agent-123")
|
|
853
|
+
resized = agent.resize(size="large")
|
|
854
|
+
|
|
855
|
+
assert resized.cpu == 4
|
|
856
|
+
assert patch_calls == [("/deployments/agent-123", {"size": "large"})]
|
|
857
|
+
|
|
858
|
+
|
|
788
859
|
def test_agents_start_preserves_generic_launch_fields(agents_client):
|
|
789
860
|
with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-generic"):
|
|
790
861
|
mock_client = MagicMock()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from hypercli.models import ModelsAPI
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DummyHTTP:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.calls = []
|
|
7
|
+
|
|
8
|
+
def get(self, path):
|
|
9
|
+
self.calls.append(("get", path, None))
|
|
10
|
+
assert path == "/v1/models"
|
|
11
|
+
return {
|
|
12
|
+
"object": "list",
|
|
13
|
+
"data": [
|
|
14
|
+
{"id": "glm-5", "object": "model", "owned_by": "hypercli"},
|
|
15
|
+
{"id": "kimi-k2.5", "object": "model", "owned_by": "hypercli"},
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_models_list_reads_openai_models_payload():
|
|
21
|
+
http = DummyHTTP()
|
|
22
|
+
models = ModelsAPI(http)
|
|
23
|
+
|
|
24
|
+
listed = models.list()
|
|
25
|
+
|
|
26
|
+
assert [model.id for model in listed] == ["glm-5", "kimi-k2.5"]
|
|
27
|
+
assert http.calls == [("get", "/v1/models", None)]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from hypercli.http import APIError
|
|
2
|
+
from hypercli.renders import Renders
|
|
3
|
+
from hypercli.http import APIError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DummyHTTP:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.calls = []
|
|
9
|
+
self.fail_once = {}
|
|
10
|
+
|
|
11
|
+
def get(self, path, params=None):
|
|
12
|
+
self.calls.append(("get", path, params))
|
|
13
|
+
if path == "/api/auth/me":
|
|
14
|
+
return self.auth_me
|
|
15
|
+
if path.endswith("/status"):
|
|
16
|
+
render_id = path.split("/")[-2]
|
|
17
|
+
return {"id": render_id, "state": "running", "progress": 0.5}
|
|
18
|
+
render_id = path.split("/")[-1]
|
|
19
|
+
return {"id": render_id, "state": "queued"}
|
|
20
|
+
|
|
21
|
+
def post(self, path, json=None):
|
|
22
|
+
self.calls.append(("post", path, json))
|
|
23
|
+
if self.fail_once.get(path):
|
|
24
|
+
error = self.fail_once.pop(path)
|
|
25
|
+
raise error
|
|
26
|
+
return {"id": "render-123", "state": "queued"}
|
|
27
|
+
|
|
28
|
+
def delete(self, path):
|
|
29
|
+
self.calls.append(("delete", path, None))
|
|
30
|
+
if self.fail_once.get(path):
|
|
31
|
+
error = self.fail_once.pop(path)
|
|
32
|
+
raise error
|
|
33
|
+
return {"status": "cancelled"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Clock:
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.now = 0.0
|
|
39
|
+
|
|
40
|
+
def time(self):
|
|
41
|
+
return self.now
|
|
42
|
+
|
|
43
|
+
def sleep(self, seconds):
|
|
44
|
+
self.now += seconds
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_create_flow_uses_subscription_route_when_available():
|
|
48
|
+
http = DummyHTTP()
|
|
49
|
+
http.auth_me = {
|
|
50
|
+
"auth_type": "orchestra_key",
|
|
51
|
+
"capabilities": ["flows:*"],
|
|
52
|
+
"has_active_subscription": True,
|
|
53
|
+
}
|
|
54
|
+
renders = Renders(http)
|
|
55
|
+
|
|
56
|
+
render = renders.create_flow("text-to-image", prompt="hello")
|
|
57
|
+
|
|
58
|
+
assert render.render_id == "render-123"
|
|
59
|
+
assert http.calls[0] == ("get", "/api/auth/me", None)
|
|
60
|
+
assert http.calls[1] == ("post", "/agents/flow/text-to-image", {"prompt": "hello"})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_create_flow_falls_back_to_paid_route_on_subscription_rejection():
|
|
64
|
+
http = DummyHTTP()
|
|
65
|
+
http.auth_me = {
|
|
66
|
+
"auth_type": "orchestra_key",
|
|
67
|
+
"capabilities": ["flows:*"],
|
|
68
|
+
"has_active_subscription": True,
|
|
69
|
+
}
|
|
70
|
+
http.fail_once["/agents/flow/text-to-image"] = APIError(403, "Active paid subscription required")
|
|
71
|
+
renders = Renders(http)
|
|
72
|
+
|
|
73
|
+
render = renders.create_flow("text-to-image", prompt="hello")
|
|
74
|
+
|
|
75
|
+
assert render.render_id == "render-123"
|
|
76
|
+
assert http.calls[1] == ("post", "/agents/flow/text-to-image", {"prompt": "hello"})
|
|
77
|
+
assert http.calls[2] == ("post", "/api/flow/text-to-image", {"prompt": "hello"})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_get_status_and_cancel_prefer_subscription_render_routes():
|
|
81
|
+
http = DummyHTTP()
|
|
82
|
+
http.auth_me = {
|
|
83
|
+
"auth_type": "user",
|
|
84
|
+
"capabilities": [],
|
|
85
|
+
"has_active_subscription": True,
|
|
86
|
+
}
|
|
87
|
+
renders = Renders(http)
|
|
88
|
+
|
|
89
|
+
render = renders.get("render-123")
|
|
90
|
+
status = renders.status("render-123")
|
|
91
|
+
cancelled = renders.cancel("render-123")
|
|
92
|
+
|
|
93
|
+
assert render.render_id == "render-123"
|
|
94
|
+
assert status.progress == 0.5
|
|
95
|
+
assert cancelled["status"] == "cancelled"
|
|
96
|
+
assert ("get", "/agents/flow/renders/render-123", None) in http.calls
|
|
97
|
+
assert ("get", "/agents/flow/renders/render-123/status", None) in http.calls
|
|
98
|
+
assert ("delete", "/agents/flow/renders/render-123", None) in http.calls
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_flow_only_keys_fallback_to_api_flow_render_routes_when_auth_me_is_denied():
|
|
102
|
+
calls = []
|
|
103
|
+
|
|
104
|
+
class FlowHTTP:
|
|
105
|
+
def get(self, path, params=None):
|
|
106
|
+
calls.append(("get", path, params))
|
|
107
|
+
if path == "/api/auth/me":
|
|
108
|
+
raise APIError(status_code=403, detail="Access denied", response_text='{"detail":"Access denied"}')
|
|
109
|
+
return {"id": "render-123", "state": "queued"}
|
|
110
|
+
|
|
111
|
+
def delete(self, path):
|
|
112
|
+
calls.append(("delete", path, None))
|
|
113
|
+
return {"status": "cancelled"}
|
|
114
|
+
|
|
115
|
+
renders = Renders(FlowHTTP())
|
|
116
|
+
|
|
117
|
+
render = renders.get("render-123")
|
|
118
|
+
status = renders.status("render-123")
|
|
119
|
+
cancelled = renders.cancel("render-123")
|
|
120
|
+
|
|
121
|
+
assert render.render_id == "render-123"
|
|
122
|
+
assert status.render_id == "render-123"
|
|
123
|
+
assert cancelled["status"] == "cancelled"
|
|
124
|
+
assert ("get", "/api/flow/renders/render-123", None) in calls
|
|
125
|
+
assert ("get", "/api/flow/renders/render-123/status", None) in calls
|
|
126
|
+
assert ("delete", "/api/flow/renders/render-123", None) in calls
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_wait_allows_queue_grace(monkeypatch):
|
|
130
|
+
http = DummyHTTP()
|
|
131
|
+
http.auth_me = {
|
|
132
|
+
"auth_type": "user",
|
|
133
|
+
"capabilities": [],
|
|
134
|
+
"has_active_subscription": True,
|
|
135
|
+
}
|
|
136
|
+
clock = Clock()
|
|
137
|
+
calls = {"count": 0}
|
|
138
|
+
|
|
139
|
+
def fake_get(path, params=None):
|
|
140
|
+
http.calls.append(("get", path, params))
|
|
141
|
+
if path == "/api/auth/me":
|
|
142
|
+
return http.auth_me
|
|
143
|
+
calls["count"] += 1
|
|
144
|
+
if calls["count"] <= 3:
|
|
145
|
+
return {"id": "render-123", "state": "running", "started_at": None}
|
|
146
|
+
return {"id": "render-123", "state": "completed", "started_at": "1970-01-01T00:00:15+00:00"}
|
|
147
|
+
|
|
148
|
+
monkeypatch.setattr(http, "get", fake_get)
|
|
149
|
+
monkeypatch.setattr("hypercli.renders.time.time", clock.time)
|
|
150
|
+
monkeypatch.setattr("hypercli.renders.time.sleep", clock.sleep)
|
|
151
|
+
renders = Renders(http)
|
|
152
|
+
|
|
153
|
+
render = renders.wait("render-123", timeout=10, poll_interval=5, queue_grace=10, active_grace=5)
|
|
154
|
+
|
|
155
|
+
assert render.state == "completed"
|
|
156
|
+
assert clock.time() == 15
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_wait_allows_recent_active_grace(monkeypatch):
|
|
160
|
+
http = DummyHTTP()
|
|
161
|
+
http.auth_me = {
|
|
162
|
+
"auth_type": "user",
|
|
163
|
+
"capabilities": [],
|
|
164
|
+
"has_active_subscription": True,
|
|
165
|
+
}
|
|
166
|
+
clock = Clock()
|
|
167
|
+
calls = {"count": 0}
|
|
168
|
+
|
|
169
|
+
def fake_get(path, params=None):
|
|
170
|
+
http.calls.append(("get", path, params))
|
|
171
|
+
if path == "/api/auth/me":
|
|
172
|
+
return http.auth_me
|
|
173
|
+
calls["count"] += 1
|
|
174
|
+
if calls["count"] == 1:
|
|
175
|
+
return {"id": "render-123", "state": "running", "started_at": None}
|
|
176
|
+
if calls["count"] == 2:
|
|
177
|
+
return {"id": "render-123", "state": "running", "started_at": "1970-01-01T00:00:04+00:00"}
|
|
178
|
+
return {"id": "render-123", "state": "completed", "started_at": "1970-01-01T00:00:04+00:00"}
|
|
179
|
+
|
|
180
|
+
monkeypatch.setattr(http, "get", fake_get)
|
|
181
|
+
monkeypatch.setattr("hypercli.renders.time.time", clock.time)
|
|
182
|
+
monkeypatch.setattr("hypercli.renders.time.sleep", clock.sleep)
|
|
183
|
+
renders = Renders(http)
|
|
184
|
+
|
|
185
|
+
render = renders.wait("render-123", timeout=5, poll_interval=5, queue_grace=5, active_grace=6)
|
|
186
|
+
|
|
187
|
+
assert render.state == "completed"
|
|
188
|
+
assert clock.time() == 10
|
|
@@ -7,8 +7,8 @@ class DummyVoiceHTTP:
|
|
|
7
7
|
def __init__(self):
|
|
8
8
|
self.calls = []
|
|
9
9
|
|
|
10
|
-
def post_bytes(self, path, json=None):
|
|
11
|
-
self.calls.append((path, json))
|
|
10
|
+
def post_bytes(self, path, json=None, timeout=None):
|
|
11
|
+
self.calls.append((path, json, timeout))
|
|
12
12
|
return b"audio-bytes"
|
|
13
13
|
|
|
14
14
|
|
|
@@ -28,6 +28,7 @@ def test_voice_tts_posts_to_agents_voice_prefix():
|
|
|
28
28
|
"language": "english",
|
|
29
29
|
"response_format": "wav",
|
|
30
30
|
},
|
|
31
|
+
VoiceAPI.DEFAULT_TIMEOUT,
|
|
31
32
|
)
|
|
32
33
|
]
|
|
33
34
|
|
|
@@ -42,11 +43,12 @@ def test_voice_clone_base64_encodes_reference(tmp_path: Path):
|
|
|
42
43
|
audio = voice.clone("clone me", ref_audio=ref, response_format="wav")
|
|
43
44
|
|
|
44
45
|
assert audio == b"audio-bytes"
|
|
45
|
-
path, payload = http.calls[0]
|
|
46
|
+
path, payload, timeout = http.calls[0]
|
|
46
47
|
assert path == "/agents/voice/clone"
|
|
47
48
|
assert payload["text"] == "clone me"
|
|
48
49
|
assert payload["response_format"] == "wav"
|
|
49
50
|
assert payload["ref_audio_base64"] == "cmVmZXJlbmNlLWF1ZGlv"
|
|
51
|
+
assert timeout == VoiceAPI.DEFAULT_TIMEOUT
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
def test_voice_design_posts_description():
|
|
@@ -65,5 +67,6 @@ def test_voice_design_posts_description():
|
|
|
65
67
|
"language": "auto",
|
|
66
68
|
"response_format": "wav",
|
|
67
69
|
},
|
|
70
|
+
VoiceAPI.DEFAULT_TIMEOUT,
|
|
68
71
|
)
|
|
69
72
|
]
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
from hypercli.http import APIError
|
|
2
|
-
from hypercli.renders import Renders
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class DummyHTTP:
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.calls = []
|
|
8
|
-
self.fail_once = {}
|
|
9
|
-
|
|
10
|
-
def get(self, path, params=None):
|
|
11
|
-
self.calls.append(("get", path, params))
|
|
12
|
-
if path == "/api/auth/me":
|
|
13
|
-
return self.auth_me
|
|
14
|
-
if path.endswith("/status"):
|
|
15
|
-
render_id = path.split("/")[-2]
|
|
16
|
-
return {"id": render_id, "state": "running", "progress": 0.5}
|
|
17
|
-
render_id = path.split("/")[-1]
|
|
18
|
-
return {"id": render_id, "state": "queued"}
|
|
19
|
-
|
|
20
|
-
def post(self, path, json=None):
|
|
21
|
-
self.calls.append(("post", path, json))
|
|
22
|
-
if self.fail_once.get(path):
|
|
23
|
-
error = self.fail_once.pop(path)
|
|
24
|
-
raise error
|
|
25
|
-
return {"id": "render-123", "state": "queued"}
|
|
26
|
-
|
|
27
|
-
def delete(self, path):
|
|
28
|
-
self.calls.append(("delete", path, None))
|
|
29
|
-
if self.fail_once.get(path):
|
|
30
|
-
error = self.fail_once.pop(path)
|
|
31
|
-
raise error
|
|
32
|
-
return {"status": "cancelled"}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_create_flow_uses_subscription_route_when_available():
|
|
36
|
-
http = DummyHTTP()
|
|
37
|
-
http.auth_me = {
|
|
38
|
-
"auth_type": "orchestra_key",
|
|
39
|
-
"capabilities": ["flows:*"],
|
|
40
|
-
"has_active_subscription": True,
|
|
41
|
-
}
|
|
42
|
-
renders = Renders(http)
|
|
43
|
-
|
|
44
|
-
render = renders.create_flow("text-to-image", prompt="hello")
|
|
45
|
-
|
|
46
|
-
assert render.render_id == "render-123"
|
|
47
|
-
assert http.calls[0] == ("get", "/api/auth/me", None)
|
|
48
|
-
assert http.calls[1] == ("post", "/agents/flow/text-to-image", {"prompt": "hello"})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_create_flow_falls_back_to_paid_route_on_subscription_rejection():
|
|
52
|
-
http = DummyHTTP()
|
|
53
|
-
http.auth_me = {
|
|
54
|
-
"auth_type": "orchestra_key",
|
|
55
|
-
"capabilities": ["flows:*"],
|
|
56
|
-
"has_active_subscription": True,
|
|
57
|
-
}
|
|
58
|
-
http.fail_once["/agents/flow/text-to-image"] = APIError(403, "Active paid subscription required")
|
|
59
|
-
renders = Renders(http)
|
|
60
|
-
|
|
61
|
-
render = renders.create_flow("text-to-image", prompt="hello")
|
|
62
|
-
|
|
63
|
-
assert render.render_id == "render-123"
|
|
64
|
-
assert http.calls[1] == ("post", "/agents/flow/text-to-image", {"prompt": "hello"})
|
|
65
|
-
assert http.calls[2] == ("post", "/api/flow/text-to-image", {"prompt": "hello"})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_get_status_and_cancel_prefer_subscription_render_routes():
|
|
69
|
-
http = DummyHTTP()
|
|
70
|
-
http.auth_me = {
|
|
71
|
-
"auth_type": "user",
|
|
72
|
-
"capabilities": [],
|
|
73
|
-
"has_active_subscription": True,
|
|
74
|
-
}
|
|
75
|
-
renders = Renders(http)
|
|
76
|
-
|
|
77
|
-
render = renders.get("render-123")
|
|
78
|
-
status = renders.status("render-123")
|
|
79
|
-
cancelled = renders.cancel("render-123")
|
|
80
|
-
|
|
81
|
-
assert render.render_id == "render-123"
|
|
82
|
-
assert status.progress == 0.5
|
|
83
|
-
assert cancelled["status"] == "cancelled"
|
|
84
|
-
assert ("get", "/agents/flow/renders/render-123", None) in http.calls
|
|
85
|
-
assert ("get", "/agents/flow/renders/render-123/status", None) in http.calls
|
|
86
|
-
assert ("delete", "/agents/flow/renders/render-123", None) in http.calls
|
|
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
|