hypercli-sdk 2026.6.16__tar.gz → 2026.6.26__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.
Potentially problematic release.
This version of hypercli-sdk might be problematic. Click here for more details.
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/.gitignore +1 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/PKG-INFO +7 -3
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/README.md +6 -2
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/__init__.py +9 -2
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/agent.py +5 -2
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/agents.py +196 -5
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/user.py +29 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/pyproject.toml +1 -1
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_agents.py +131 -6
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_claw.py +16 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_keys.py +5 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/_compat.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/client.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/gateway.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/models.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/openclaw/__init__.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/openclaw/gateway.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/renders.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/voice.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_agents.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_bootstrap_console_test_key.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_bootstrap_dev_test_keys.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_gateway.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_models.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_renders_subscription.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_status.py +0 -0
- {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_voice.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-sdk
|
|
3
|
-
Version: 2026.6.
|
|
3
|
+
Version: 2026.6.26
|
|
4
4
|
Summary: Python SDK for HyperCLI - GPU orchestration and HyperAgent API
|
|
5
5
|
Project-URL: Homepage, https://hypercli.com
|
|
6
6
|
Project-URL: Documentation, https://docs.hypercli.com
|
|
@@ -121,6 +121,7 @@ from openai import OpenAI
|
|
|
121
121
|
sdk = HyperCLI(api_key="hyper_api_key", agent_api_key="sk-agent")
|
|
122
122
|
plans = sdk.agent.plans()
|
|
123
123
|
activation = sdk.agent.redeem_grant_code("PROMO123")
|
|
124
|
+
renewal = sdk.agent.redeem_grant_code("PROMO123", extend_existing=True)
|
|
124
125
|
|
|
125
126
|
client = OpenAI(
|
|
126
127
|
api_key="your_hyperagent_api_key",
|
|
@@ -133,7 +134,7 @@ response = client.chat.completions.create(
|
|
|
133
134
|
)
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
`redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement.
|
|
137
|
+
`redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement. Codes create new entitlements by default; pass `extend_existing=True` only for renewal/extension behavior.
|
|
137
138
|
|
|
138
139
|
## OpenClaw Agents
|
|
139
140
|
|
|
@@ -143,14 +144,17 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
|
|
|
143
144
|
agent = client.deployments.create_openclaw(
|
|
144
145
|
name="docs-demo",
|
|
145
146
|
start=True,
|
|
146
|
-
heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
|
|
147
147
|
registry_url="git.nedos.co",
|
|
148
148
|
registry_auth={"username": "ci", "password": "token"},
|
|
149
149
|
)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
Use `create_openclaw_pro(...)` for the desktop/browser image. It enables noVNC through the protected `desktop-<agent>.hypercli.app` route and sets `OPENCLAW_DESKTOP_ENABLED=1`.
|
|
153
|
+
|
|
152
154
|
`heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
|
|
153
155
|
|
|
156
|
+
Automatic memory indexing is off by default. Opt in with `memory_index={"on_session_start": True, "on_search": True, "watch": True, "watch_debounce_ms": 30000, "interval_minutes": 0}`.
|
|
157
|
+
|
|
154
158
|
## Error Handling
|
|
155
159
|
|
|
156
160
|
```python
|
|
@@ -90,6 +90,7 @@ from openai import OpenAI
|
|
|
90
90
|
sdk = HyperCLI(api_key="hyper_api_key", agent_api_key="sk-agent")
|
|
91
91
|
plans = sdk.agent.plans()
|
|
92
92
|
activation = sdk.agent.redeem_grant_code("PROMO123")
|
|
93
|
+
renewal = sdk.agent.redeem_grant_code("PROMO123", extend_existing=True)
|
|
93
94
|
|
|
94
95
|
client = OpenAI(
|
|
95
96
|
api_key="your_hyperagent_api_key",
|
|
@@ -102,7 +103,7 @@ response = client.chat.completions.create(
|
|
|
102
103
|
)
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
`redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement.
|
|
106
|
+
`redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement. Codes create new entitlements by default; pass `extend_existing=True` only for renewal/extension behavior.
|
|
106
107
|
|
|
107
108
|
## OpenClaw Agents
|
|
108
109
|
|
|
@@ -112,14 +113,17 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
|
|
|
112
113
|
agent = client.deployments.create_openclaw(
|
|
113
114
|
name="docs-demo",
|
|
114
115
|
start=True,
|
|
115
|
-
heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
|
|
116
116
|
registry_url="git.nedos.co",
|
|
117
117
|
registry_auth={"username": "ci", "password": "token"},
|
|
118
118
|
)
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
Use `create_openclaw_pro(...)` for the desktop/browser image. It enables noVNC through the protected `desktop-<agent>.hypercli.app` route and sets `OPENCLAW_DESKTOP_ENABLED=1`.
|
|
122
|
+
|
|
121
123
|
`heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
|
|
122
124
|
|
|
125
|
+
Automatic memory indexing is off by default. Opt in with `memory_index={"on_session_start": True, "on_search": True, "watch": True, "watch_debounce_ms": 30000, "interval_minutes": 0}`.
|
|
126
|
+
|
|
123
127
|
## Error Handling
|
|
124
128
|
|
|
125
129
|
```python
|
|
@@ -39,9 +39,10 @@ from .voice import VoiceAPI
|
|
|
39
39
|
from .models import Model, ModelsAPI
|
|
40
40
|
from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, FlowCatalogItem
|
|
41
41
|
from .files import File, AsyncFiles
|
|
42
|
+
from .user import AuthMe, RuntimeIdentity, User, UserAPI
|
|
42
43
|
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
|
|
43
44
|
from .logs import LogStream, stream_logs, fetch_logs
|
|
44
|
-
from .agents import Deployments, Agent, OpenClawAgent, ExecResult, build_openclaw_routes
|
|
45
|
+
from .agents import Deployments, Agent, OpenClawAgent, OpenClawProAgent, ExecResult, build_openclaw_routes, build_openclaw_memory_index_env
|
|
45
46
|
from .shell import ShellSession, shell_connect
|
|
46
47
|
from .agent import (
|
|
47
48
|
HyperAgent,
|
|
@@ -82,7 +83,7 @@ from .gateway import (
|
|
|
82
83
|
extract_gateway_chat_tool_calls,
|
|
83
84
|
normalize_gateway_chat_message,
|
|
84
85
|
)
|
|
85
|
-
__version__ = "2026.6.
|
|
86
|
+
__version__ = "2026.6.26"
|
|
86
87
|
__all__ = [
|
|
87
88
|
"HyperCLI",
|
|
88
89
|
"configure",
|
|
@@ -129,6 +130,10 @@ __all__ = [
|
|
|
129
130
|
# Files API
|
|
130
131
|
"File",
|
|
131
132
|
"AsyncFiles",
|
|
133
|
+
"User",
|
|
134
|
+
"UserAPI",
|
|
135
|
+
"AuthMe",
|
|
136
|
+
"RuntimeIdentity",
|
|
132
137
|
"AsyncHTTPClient",
|
|
133
138
|
# Job lookup utils
|
|
134
139
|
"find_job",
|
|
@@ -156,8 +161,10 @@ __all__ = [
|
|
|
156
161
|
"Deployments",
|
|
157
162
|
"Agent",
|
|
158
163
|
"OpenClawAgent",
|
|
164
|
+
"OpenClawProAgent",
|
|
159
165
|
"ExecResult",
|
|
160
166
|
"build_openclaw_routes",
|
|
167
|
+
"build_openclaw_memory_index_env",
|
|
161
168
|
# Shell
|
|
162
169
|
"ShellSession",
|
|
163
170
|
"shell_connect",
|
|
@@ -884,11 +884,14 @@ class HyperAgent:
|
|
|
884
884
|
response.raise_for_status()
|
|
885
885
|
return response.json()
|
|
886
886
|
|
|
887
|
-
def redeem_grant_code(self, code: str) -> Dict[str, Any]:
|
|
887
|
+
def redeem_grant_code(self, code: str, *, extend_existing: bool | None = None) -> Dict[str, Any]:
|
|
888
|
+
payload: dict[str, Any] = {"code": str(code)}
|
|
889
|
+
if extend_existing is not None:
|
|
890
|
+
payload["extend_existing"] = bool(extend_existing)
|
|
888
891
|
response = self._http._session.post(
|
|
889
892
|
f"{self._control_base_url}/billing/grants/redeem",
|
|
890
893
|
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
891
|
-
json=
|
|
894
|
+
json=payload,
|
|
892
895
|
)
|
|
893
896
|
response.raise_for_status()
|
|
894
897
|
return response.json()
|
|
@@ -31,6 +31,15 @@ AGENTS_WS_URL = "wss://api.agents.hypercli.com/ws"
|
|
|
31
31
|
DEV_AGENTS_API_BASE = "https://api.dev.hypercli.com/agents"
|
|
32
32
|
DEV_AGENTS_WS_URL = "wss://api.agents.dev.hypercli.com/ws"
|
|
33
33
|
DEFAULT_OPENCLAW_IMAGE = "ghcr.io/hypercli/hypercli-openclaw:prod"
|
|
34
|
+
DEFAULT_OPENCLAW_PRO_IMAGE = "ghcr.io/hypercli/hypercli-openclaw:pro-prod"
|
|
35
|
+
OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
|
|
36
|
+
"OPENCLAW_MEMORY_SEARCH_ENABLED": "1",
|
|
37
|
+
"OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START": "0",
|
|
38
|
+
"OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH": "0",
|
|
39
|
+
"OPENCLAW_MEMORY_SEARCH_SYNC_WATCH": "0",
|
|
40
|
+
"OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS": "30000",
|
|
41
|
+
"OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES": "0",
|
|
42
|
+
}
|
|
34
43
|
LAUNCH_CONFIG_KEYS = frozenset({"image", "env", "routes", "ports", "command", "entrypoint", "sync_root", "sync_enabled", "sync_uid", "sync_gid", "registry_url", "registry_auth"})
|
|
35
44
|
DEFAULT_OPENCLAW_SYNC_ROOT = "/home/node"
|
|
36
45
|
AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024
|
|
@@ -87,6 +96,48 @@ def _resolve_openclaw_routes(
|
|
|
87
96
|
return build_openclaw_routes(**dict(openclaw_route_options or {}))
|
|
88
97
|
|
|
89
98
|
|
|
99
|
+
def _env_bool(value: object) -> str:
|
|
100
|
+
return "1" if bool(value) else "0"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _env_non_negative_int(name: str, value: object) -> str:
|
|
104
|
+
integer = int(value)
|
|
105
|
+
if integer < 0:
|
|
106
|
+
raise ValueError(f"{name} must be non-negative")
|
|
107
|
+
return str(integer)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_openclaw_memory_index_env(memory_index: dict | None = None) -> dict[str, str]:
|
|
111
|
+
"""Build OpenClaw memory-search indexing environment variables.
|
|
112
|
+
|
|
113
|
+
No env vars are emitted unless memory_index is provided; the image config
|
|
114
|
+
carries the no-auto-indexing defaults. Passing an empty dict explicitly
|
|
115
|
+
emits the default env block.
|
|
116
|
+
"""
|
|
117
|
+
if memory_index is None:
|
|
118
|
+
return {}
|
|
119
|
+
env = dict(OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS)
|
|
120
|
+
if memory_index.get("enabled") is not None:
|
|
121
|
+
env["OPENCLAW_MEMORY_SEARCH_ENABLED"] = _env_bool(memory_index["enabled"])
|
|
122
|
+
if memory_index.get("on_session_start") is not None:
|
|
123
|
+
env["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START"] = _env_bool(memory_index["on_session_start"])
|
|
124
|
+
if memory_index.get("on_search") is not None:
|
|
125
|
+
env["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH"] = _env_bool(memory_index["on_search"])
|
|
126
|
+
if memory_index.get("watch") is not None:
|
|
127
|
+
env["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH"] = _env_bool(memory_index["watch"])
|
|
128
|
+
if memory_index.get("watch_debounce_ms") is not None:
|
|
129
|
+
env["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS"] = _env_non_negative_int(
|
|
130
|
+
"watch_debounce_ms",
|
|
131
|
+
memory_index["watch_debounce_ms"],
|
|
132
|
+
)
|
|
133
|
+
if memory_index.get("interval_minutes") is not None:
|
|
134
|
+
env["OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES"] = _env_non_negative_int(
|
|
135
|
+
"interval_minutes",
|
|
136
|
+
memory_index["interval_minutes"],
|
|
137
|
+
)
|
|
138
|
+
return env
|
|
139
|
+
|
|
140
|
+
|
|
90
141
|
def _default_gateway_timeout() -> float | None:
|
|
91
142
|
raw = (
|
|
92
143
|
os.environ.get("HYPERCLI_GATEWAY_TIMEOUT")
|
|
@@ -259,6 +310,16 @@ def _default_openclaw_image(image: str | None) -> str | None:
|
|
|
259
310
|
return DEFAULT_OPENCLAW_IMAGE
|
|
260
311
|
|
|
261
312
|
|
|
313
|
+
def _default_openclaw_pro_image(image: str | None) -> str | None:
|
|
314
|
+
if image is not None:
|
|
315
|
+
return image
|
|
316
|
+
return DEFAULT_OPENCLAW_PRO_IMAGE
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _truthy_env(value: object) -> bool:
|
|
320
|
+
return str(value or "").strip().lower() in {"1", "true", "yes", "on", "enabled"}
|
|
321
|
+
|
|
322
|
+
|
|
262
323
|
def _parse_dt(val):
|
|
263
324
|
if isinstance(val, str) and val:
|
|
264
325
|
return datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
@@ -317,6 +378,17 @@ def _is_openclaw_agent_data(data: dict) -> bool:
|
|
|
317
378
|
return False
|
|
318
379
|
|
|
319
380
|
|
|
381
|
+
def _is_openclaw_pro_agent_data(data: dict) -> bool:
|
|
382
|
+
launch_config = data.get("launch_config")
|
|
383
|
+
if not isinstance(launch_config, dict):
|
|
384
|
+
return False
|
|
385
|
+
env = launch_config.get("env")
|
|
386
|
+
if isinstance(env, dict) and _truthy_env(env.get("OPENCLAW_DESKTOP_ENABLED")):
|
|
387
|
+
return True
|
|
388
|
+
image = str(launch_config.get("image") or "")
|
|
389
|
+
return "hypercli-openclaw:pro" in image or image.endswith("-pro")
|
|
390
|
+
|
|
391
|
+
|
|
320
392
|
@dataclass
|
|
321
393
|
class Agent:
|
|
322
394
|
"""Generic agent returned by the HyperClaw backend."""
|
|
@@ -953,6 +1025,11 @@ class OpenClawAgent(Agent):
|
|
|
953
1025
|
return await self.channel_upsert("discord", channel_config, account_id=account_id)
|
|
954
1026
|
|
|
955
1027
|
|
|
1028
|
+
@dataclass
|
|
1029
|
+
class OpenClawProAgent(OpenClawAgent):
|
|
1030
|
+
"""OpenClaw agent launched with the pro desktop/browser preset."""
|
|
1031
|
+
|
|
1032
|
+
|
|
956
1033
|
@dataclass
|
|
957
1034
|
class ExecResult:
|
|
958
1035
|
"""Result of a one-shot command execution."""
|
|
@@ -1009,7 +1086,9 @@ class Deployments:
|
|
|
1009
1086
|
)
|
|
1010
1087
|
|
|
1011
1088
|
def _hydrate_agent(self, data: dict) -> Agent:
|
|
1012
|
-
if
|
|
1089
|
+
if _is_openclaw_pro_agent_data(data):
|
|
1090
|
+
agent = OpenClawProAgent.from_dict(data)
|
|
1091
|
+
elif _is_openclaw_agent_data(data):
|
|
1013
1092
|
agent = OpenClawAgent.from_dict(data)
|
|
1014
1093
|
else:
|
|
1015
1094
|
agent = Agent.from_dict(data)
|
|
@@ -1182,8 +1261,9 @@ class Deployments:
|
|
|
1182
1261
|
start: bool = True,
|
|
1183
1262
|
openclaw_routes: dict | None = None,
|
|
1184
1263
|
openclaw_route_options: dict | None = None,
|
|
1264
|
+
memory_index: dict | None = None,
|
|
1185
1265
|
) -> Agent:
|
|
1186
|
-
effective_env = dict(env or {})
|
|
1266
|
+
effective_env = {**build_openclaw_memory_index_env(memory_index), **dict(env or {})}
|
|
1187
1267
|
return self.create(
|
|
1188
1268
|
name=name,
|
|
1189
1269
|
size=size,
|
|
@@ -1212,6 +1292,62 @@ class Deployments:
|
|
|
1212
1292
|
start=start,
|
|
1213
1293
|
)
|
|
1214
1294
|
|
|
1295
|
+
def create_openclaw_pro(
|
|
1296
|
+
self,
|
|
1297
|
+
name: str = None,
|
|
1298
|
+
size: str = None,
|
|
1299
|
+
config: dict = None,
|
|
1300
|
+
tags: list[str] = None,
|
|
1301
|
+
env: dict = None,
|
|
1302
|
+
ports: list = None,
|
|
1303
|
+
routes: dict = None,
|
|
1304
|
+
command: list[str] = None,
|
|
1305
|
+
entrypoint: list[str] = None,
|
|
1306
|
+
image: str = None,
|
|
1307
|
+
sync_root: str = None,
|
|
1308
|
+
sync_enabled: bool = None,
|
|
1309
|
+
sync_uid: int = None,
|
|
1310
|
+
sync_gid: int = None,
|
|
1311
|
+
registry_url: str = None,
|
|
1312
|
+
registry_auth: dict = None,
|
|
1313
|
+
gateway_token: str = None,
|
|
1314
|
+
heartbeat: dict = None,
|
|
1315
|
+
meta_ui: dict = None,
|
|
1316
|
+
dry_run: bool = False,
|
|
1317
|
+
start: bool = True,
|
|
1318
|
+
openclaw_routes: dict | None = None,
|
|
1319
|
+
openclaw_route_options: dict | None = None,
|
|
1320
|
+
memory_index: dict | None = None,
|
|
1321
|
+
) -> Agent:
|
|
1322
|
+
effective_env = {"OPENCLAW_DESKTOP_ENABLED": "1", **dict(env or {})}
|
|
1323
|
+
effective_route_options = {"include_desktop": True, **dict(openclaw_route_options or {})}
|
|
1324
|
+
return self.create_openclaw(
|
|
1325
|
+
name=name,
|
|
1326
|
+
size=size,
|
|
1327
|
+
config=config,
|
|
1328
|
+
tags=tags,
|
|
1329
|
+
env=effective_env,
|
|
1330
|
+
ports=ports,
|
|
1331
|
+
routes=routes,
|
|
1332
|
+
command=command,
|
|
1333
|
+
entrypoint=entrypoint,
|
|
1334
|
+
image=_default_openclaw_pro_image(image),
|
|
1335
|
+
sync_root=sync_root,
|
|
1336
|
+
sync_enabled=sync_enabled,
|
|
1337
|
+
sync_uid=sync_uid,
|
|
1338
|
+
sync_gid=sync_gid,
|
|
1339
|
+
registry_url=registry_url,
|
|
1340
|
+
registry_auth=registry_auth,
|
|
1341
|
+
gateway_token=gateway_token,
|
|
1342
|
+
heartbeat=heartbeat,
|
|
1343
|
+
meta_ui=meta_ui,
|
|
1344
|
+
dry_run=dry_run,
|
|
1345
|
+
start=start,
|
|
1346
|
+
openclaw_routes=openclaw_routes,
|
|
1347
|
+
openclaw_route_options=effective_route_options,
|
|
1348
|
+
memory_index=memory_index,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1215
1351
|
def budget(self) -> dict:
|
|
1216
1352
|
"""Get the user's current agent resource budget and usage.
|
|
1217
1353
|
|
|
@@ -1345,8 +1481,9 @@ class Deployments:
|
|
|
1345
1481
|
dry_run: bool = False,
|
|
1346
1482
|
openclaw_routes: dict | None = None,
|
|
1347
1483
|
openclaw_route_options: dict | None = None,
|
|
1484
|
+
memory_index: dict | None = None,
|
|
1348
1485
|
) -> Agent:
|
|
1349
|
-
effective_env = dict(env or {})
|
|
1486
|
+
effective_env = {**build_openclaw_memory_index_env(memory_index), **dict(env or {})}
|
|
1350
1487
|
return self.start(
|
|
1351
1488
|
agent_id,
|
|
1352
1489
|
config=config,
|
|
@@ -1371,6 +1508,54 @@ class Deployments:
|
|
|
1371
1508
|
dry_run=dry_run,
|
|
1372
1509
|
)
|
|
1373
1510
|
|
|
1511
|
+
def start_openclaw_pro(
|
|
1512
|
+
self,
|
|
1513
|
+
agent_id: str,
|
|
1514
|
+
config: dict = None,
|
|
1515
|
+
env: dict = None,
|
|
1516
|
+
ports: list = None,
|
|
1517
|
+
routes: dict = None,
|
|
1518
|
+
command: list[str] = None,
|
|
1519
|
+
entrypoint: list[str] = None,
|
|
1520
|
+
image: str = None,
|
|
1521
|
+
sync_root: str = None,
|
|
1522
|
+
sync_enabled: bool = None,
|
|
1523
|
+
sync_uid: int = None,
|
|
1524
|
+
sync_gid: int = None,
|
|
1525
|
+
registry_url: str = None,
|
|
1526
|
+
registry_auth: dict = None,
|
|
1527
|
+
gateway_token: str = None,
|
|
1528
|
+
heartbeat: dict = None,
|
|
1529
|
+
dry_run: bool = False,
|
|
1530
|
+
openclaw_routes: dict | None = None,
|
|
1531
|
+
openclaw_route_options: dict | None = None,
|
|
1532
|
+
memory_index: dict | None = None,
|
|
1533
|
+
) -> Agent:
|
|
1534
|
+
effective_env = {"OPENCLAW_DESKTOP_ENABLED": "1", **dict(env or {})}
|
|
1535
|
+
effective_route_options = {"include_desktop": True, **dict(openclaw_route_options or {})}
|
|
1536
|
+
return self.start_openclaw(
|
|
1537
|
+
agent_id,
|
|
1538
|
+
config=config,
|
|
1539
|
+
env=effective_env,
|
|
1540
|
+
ports=ports,
|
|
1541
|
+
routes=routes,
|
|
1542
|
+
command=command,
|
|
1543
|
+
entrypoint=entrypoint,
|
|
1544
|
+
image=_default_openclaw_pro_image(image),
|
|
1545
|
+
sync_root=sync_root,
|
|
1546
|
+
sync_enabled=sync_enabled,
|
|
1547
|
+
sync_uid=sync_uid,
|
|
1548
|
+
sync_gid=sync_gid,
|
|
1549
|
+
registry_url=registry_url,
|
|
1550
|
+
registry_auth=registry_auth,
|
|
1551
|
+
gateway_token=gateway_token,
|
|
1552
|
+
heartbeat=heartbeat,
|
|
1553
|
+
dry_run=dry_run,
|
|
1554
|
+
openclaw_routes=openclaw_routes,
|
|
1555
|
+
openclaw_route_options=effective_route_options,
|
|
1556
|
+
memory_index=memory_index,
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1374
1559
|
def update(
|
|
1375
1560
|
self,
|
|
1376
1561
|
agent_id: str,
|
|
@@ -1472,14 +1657,20 @@ class Deployments:
|
|
|
1472
1657
|
*,
|
|
1473
1658
|
duration: int,
|
|
1474
1659
|
tags: list[str] | None = None,
|
|
1660
|
+
extend_existing: bool | None = None,
|
|
1475
1661
|
) -> dict:
|
|
1476
1662
|
payload: dict[str, Any] = {"duration": int(duration)}
|
|
1477
1663
|
if tags is not None:
|
|
1478
1664
|
payload["tags"] = list(tags)
|
|
1665
|
+
if extend_existing is not None:
|
|
1666
|
+
payload["extend_existing"] = bool(extend_existing)
|
|
1479
1667
|
return self._post(f"/billing/balance/{quote(str(plan_id), safe='')}", json=payload)
|
|
1480
1668
|
|
|
1481
|
-
def redeem_grant_code(self, code: str) -> dict:
|
|
1482
|
-
|
|
1669
|
+
def redeem_grant_code(self, code: str, *, extend_existing: bool | None = None) -> dict:
|
|
1670
|
+
payload: dict[str, Any] = {"code": str(code)}
|
|
1671
|
+
if extend_existing is not None:
|
|
1672
|
+
payload["extend_existing"] = bool(extend_existing)
|
|
1673
|
+
return self._post("/billing/grants/redeem", json=payload)
|
|
1483
1674
|
|
|
1484
1675
|
def logs_token(self, agent_id: str) -> dict:
|
|
1485
1676
|
"""Mint a short-lived JWT token for backend log streaming."""
|
|
@@ -25,6 +25,21 @@ class User:
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@dataclass
|
|
29
|
+
class RuntimeIdentity:
|
|
30
|
+
kind: str
|
|
31
|
+
agent_id: str | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict | None) -> "RuntimeIdentity | None":
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return None
|
|
37
|
+
kind = str(data.get("kind") or "").strip()
|
|
38
|
+
if not kind:
|
|
39
|
+
return None
|
|
40
|
+
return cls(kind=kind, agent_id=data.get("agent_id"))
|
|
41
|
+
|
|
42
|
+
|
|
28
43
|
@dataclass
|
|
29
44
|
class AuthMe:
|
|
30
45
|
user_id: str
|
|
@@ -34,6 +49,8 @@ class AuthMe:
|
|
|
34
49
|
email: str | None
|
|
35
50
|
auth_type: str
|
|
36
51
|
capabilities: list[str]
|
|
52
|
+
tags: list[str]
|
|
53
|
+
runtime: RuntimeIdentity | None
|
|
37
54
|
has_active_subscription: bool
|
|
38
55
|
key_id: str | None
|
|
39
56
|
key_name: str | None
|
|
@@ -48,11 +65,23 @@ class AuthMe:
|
|
|
48
65
|
email=data.get("email"),
|
|
49
66
|
auth_type=data.get("auth_type", ""),
|
|
50
67
|
capabilities=list(data.get("capabilities") or []),
|
|
68
|
+
tags=list(data.get("tags") or []),
|
|
69
|
+
runtime=RuntimeIdentity.from_dict(data.get("runtime")),
|
|
51
70
|
has_active_subscription=bool(data.get("has_active_subscription")),
|
|
52
71
|
key_id=data.get("key_id"),
|
|
53
72
|
key_name=data.get("key_name"),
|
|
54
73
|
)
|
|
55
74
|
|
|
75
|
+
@property
|
|
76
|
+
def is_runtime_agent(self) -> bool:
|
|
77
|
+
return self.runtime is not None and self.runtime.kind == "agent"
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def runtime_agent_id(self) -> str | None:
|
|
81
|
+
if not self.is_runtime_agent or self.runtime is None:
|
|
82
|
+
return None
|
|
83
|
+
return self.runtime.agent_id
|
|
84
|
+
|
|
56
85
|
|
|
57
86
|
class UserAPI:
|
|
58
87
|
"""User API wrapper"""
|
|
@@ -15,8 +15,10 @@ from hypercli.agents import (
|
|
|
15
15
|
AGENT_FILE_TRANSFER_CHUNK_BYTES,
|
|
16
16
|
Agent,
|
|
17
17
|
DEFAULT_OPENCLAW_IMAGE,
|
|
18
|
+
DEFAULT_OPENCLAW_PRO_IMAGE,
|
|
18
19
|
Deployments,
|
|
19
20
|
OpenClawAgent,
|
|
21
|
+
OpenClawProAgent,
|
|
20
22
|
ExecResult,
|
|
21
23
|
_build_agent_launch,
|
|
22
24
|
build_openclaw_routes,
|
|
@@ -454,7 +456,7 @@ def test_build_agent_launch_includes_command_and_entrypoint():
|
|
|
454
456
|
def test_build_agent_launch_merges_heartbeat_defaults():
|
|
455
457
|
launch, _gateway_token = _build_agent_launch(
|
|
456
458
|
{"agents": {"defaults": {"model": "openai/gpt-5.4", "heartbeat": {"target": "last"}}}},
|
|
457
|
-
heartbeat={"every": "
|
|
459
|
+
heartbeat={"every": "1h", "target": "last"},
|
|
458
460
|
gateway_token="gw-token",
|
|
459
461
|
)
|
|
460
462
|
|
|
@@ -464,8 +466,7 @@ def test_build_agent_launch_merges_heartbeat_defaults():
|
|
|
464
466
|
"model": "openai/gpt-5.4",
|
|
465
467
|
"heartbeat": {
|
|
466
468
|
"target": "last",
|
|
467
|
-
"every": "
|
|
468
|
-
"includeSystemPromptSection": False,
|
|
469
|
+
"every": "1h",
|
|
469
470
|
},
|
|
470
471
|
}
|
|
471
472
|
}
|
|
@@ -543,6 +544,81 @@ def test_create_openclaw_respects_explicit_empty_routes(agents_client):
|
|
|
543
544
|
assert posted_json["routes"] == {}
|
|
544
545
|
|
|
545
546
|
|
|
547
|
+
def test_create_openclaw_pro_defaults_desktop_image_env_and_routes(agents_client):
|
|
548
|
+
with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
|
|
549
|
+
mock_client = MagicMock()
|
|
550
|
+
mock_response = Mock()
|
|
551
|
+
mock_response.status_code = 200
|
|
552
|
+
mock_response.json.return_value = {
|
|
553
|
+
"id": "agent-123",
|
|
554
|
+
"user_id": "user-456",
|
|
555
|
+
"pod_id": "pod-789",
|
|
556
|
+
"pod_name": "test-pod",
|
|
557
|
+
"state": "starting",
|
|
558
|
+
"launch_config": {
|
|
559
|
+
"image": DEFAULT_OPENCLAW_PRO_IMAGE,
|
|
560
|
+
"env": {"OPENCLAW_DESKTOP_ENABLED": "1"},
|
|
561
|
+
"routes": {"openclaw": {"port": 18789, "auth": False, "prefix": ""}},
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
mock_client.post.return_value = mock_response
|
|
565
|
+
mock_client.__enter__.return_value = mock_client
|
|
566
|
+
mock_client.__exit__.return_value = False
|
|
567
|
+
mock_client_class.return_value = mock_client
|
|
568
|
+
|
|
569
|
+
agent = agents_client.create_openclaw_pro(name="test-agent")
|
|
570
|
+
|
|
571
|
+
posted_json = mock_client.post.call_args[1]["json"]
|
|
572
|
+
assert posted_json["image"] == DEFAULT_OPENCLAW_PRO_IMAGE
|
|
573
|
+
assert posted_json["env"]["OPENCLAW_DESKTOP_ENABLED"] == "1"
|
|
574
|
+
assert "OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START" not in posted_json["env"]
|
|
575
|
+
assert posted_json["routes"] == {
|
|
576
|
+
"openclaw": {"port": 18789, "auth": False, "prefix": ""},
|
|
577
|
+
"desktop": {"port": 3000, "auth": True, "prefix": "desktop"},
|
|
578
|
+
}
|
|
579
|
+
assert isinstance(agent, OpenClawProAgent)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def test_create_openclaw_accepts_memory_index_options(agents_client):
|
|
583
|
+
with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
|
|
584
|
+
mock_client = MagicMock()
|
|
585
|
+
mock_response = Mock()
|
|
586
|
+
mock_response.status_code = 200
|
|
587
|
+
mock_response.json.return_value = {
|
|
588
|
+
"id": "agent-123",
|
|
589
|
+
"user_id": "user-456",
|
|
590
|
+
"pod_id": "pod-789",
|
|
591
|
+
"pod_name": "test-pod",
|
|
592
|
+
"state": "starting",
|
|
593
|
+
"launch_config": {
|
|
594
|
+
"env": {},
|
|
595
|
+
"routes": {"openclaw": {"port": 18789, "auth": False, "prefix": ""}},
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
mock_client.post.return_value = mock_response
|
|
599
|
+
mock_client.__enter__.return_value = mock_client
|
|
600
|
+
mock_client.__exit__.return_value = False
|
|
601
|
+
mock_client_class.return_value = mock_client
|
|
602
|
+
|
|
603
|
+
agents_client.create_openclaw(
|
|
604
|
+
name="test-agent",
|
|
605
|
+
memory_index={
|
|
606
|
+
"on_session_start": True,
|
|
607
|
+
"on_search": True,
|
|
608
|
+
"watch": True,
|
|
609
|
+
"watch_debounce_ms": 60000,
|
|
610
|
+
"interval_minutes": 120,
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
posted_json = mock_client.post.call_args[1]["json"]
|
|
615
|
+
assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START"] == "1"
|
|
616
|
+
assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH"] == "1"
|
|
617
|
+
assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH"] == "1"
|
|
618
|
+
assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS"] == "60000"
|
|
619
|
+
assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES"] == "120"
|
|
620
|
+
|
|
621
|
+
|
|
546
622
|
def test_create_openclaw_includes_heartbeat_when_requested(agents_client):
|
|
547
623
|
with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
|
|
548
624
|
mock_client = MagicMock()
|
|
@@ -562,13 +638,13 @@ def test_create_openclaw_includes_heartbeat_when_requested(agents_client):
|
|
|
562
638
|
|
|
563
639
|
agents_client.create_openclaw(
|
|
564
640
|
name="test-agent",
|
|
565
|
-
heartbeat={"every": "
|
|
641
|
+
heartbeat={"every": "1h", "target": "last"},
|
|
566
642
|
)
|
|
567
643
|
|
|
568
644
|
posted_json = mock_client.post.call_args[1]["json"]
|
|
569
645
|
assert posted_json["config"]["agents"]["defaults"]["heartbeat"] == {
|
|
570
|
-
"every": "
|
|
571
|
-
"
|
|
646
|
+
"every": "1h",
|
|
647
|
+
"target": "last",
|
|
572
648
|
}
|
|
573
649
|
|
|
574
650
|
@pytest.fixture
|
|
@@ -1111,6 +1187,35 @@ def test_agents_purchase_entitlement_from_balance(agents_client):
|
|
|
1111
1187
|
assert mock_client.post.call_args[1]["json"] == {"duration": 3600, "tags": ["customer=acme"]}
|
|
1112
1188
|
|
|
1113
1189
|
|
|
1190
|
+
def test_agents_purchase_entitlement_from_balance_can_extend_existing(agents_client):
|
|
1191
|
+
with patch("httpx.Client") as mock_client_class:
|
|
1192
|
+
mock_client = MagicMock()
|
|
1193
|
+
mock_response = Mock()
|
|
1194
|
+
mock_response.status_code = 200
|
|
1195
|
+
mock_response.json.return_value = {
|
|
1196
|
+
"grant": {"id": "grant-1", "type": "BALANCE"},
|
|
1197
|
+
"entitlement": {"id": "ent-1", "plan_id": "basic"},
|
|
1198
|
+
}
|
|
1199
|
+
mock_client.post.return_value = mock_response
|
|
1200
|
+
mock_client.__enter__.return_value = mock_client
|
|
1201
|
+
mock_client.__exit__.return_value = False
|
|
1202
|
+
mock_client_class.return_value = mock_client
|
|
1203
|
+
|
|
1204
|
+
agents_client.purchase_entitlement_from_balance(
|
|
1205
|
+
"basic",
|
|
1206
|
+
duration=3600,
|
|
1207
|
+
tags=["customer=acme"],
|
|
1208
|
+
extend_existing=True,
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
assert mock_client.post.call_args[0][0].endswith("/billing/balance/basic")
|
|
1212
|
+
assert mock_client.post.call_args[1]["json"] == {
|
|
1213
|
+
"duration": 3600,
|
|
1214
|
+
"tags": ["customer=acme"],
|
|
1215
|
+
"extend_existing": True,
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
|
|
1114
1219
|
def test_agents_redeem_grant_code(agents_client):
|
|
1115
1220
|
with patch("httpx.Client") as mock_client_class:
|
|
1116
1221
|
mock_client = MagicMock()
|
|
@@ -1132,6 +1237,26 @@ def test_agents_redeem_grant_code(agents_client):
|
|
|
1132
1237
|
assert mock_client.post.call_args[1]["json"] == {"code": "promo-123"}
|
|
1133
1238
|
|
|
1134
1239
|
|
|
1240
|
+
def test_agents_redeem_grant_code_can_request_extension(agents_client):
|
|
1241
|
+
with patch("httpx.Client") as mock_client_class:
|
|
1242
|
+
mock_client = MagicMock()
|
|
1243
|
+
mock_response = Mock()
|
|
1244
|
+
mock_response.status_code = 200
|
|
1245
|
+
mock_response.json.return_value = {
|
|
1246
|
+
"grant": {"id": "grant-1", "type": "ACTIVATION_CODE", "code": "promo-123"},
|
|
1247
|
+
"entitlement": {"id": "ent-1", "plan_id": "basic"},
|
|
1248
|
+
}
|
|
1249
|
+
mock_client.post.return_value = mock_response
|
|
1250
|
+
mock_client.__enter__.return_value = mock_client
|
|
1251
|
+
mock_client.__exit__.return_value = False
|
|
1252
|
+
mock_client_class.return_value = mock_client
|
|
1253
|
+
|
|
1254
|
+
agents_client.redeem_grant_code("promo-123", extend_existing=True)
|
|
1255
|
+
|
|
1256
|
+
assert mock_client.post.call_args[0][0].endswith("/billing/grants/redeem")
|
|
1257
|
+
assert mock_client.post.call_args[1]["json"] == {"code": "promo-123", "extend_existing": True}
|
|
1258
|
+
|
|
1259
|
+
|
|
1135
1260
|
def test_openclaw_agent_resolve_gateway_token_uses_env_route():
|
|
1136
1261
|
manager = Mock()
|
|
1137
1262
|
manager.get.return_value = OpenClawAgent.from_dict({
|
|
@@ -379,6 +379,22 @@ class TestHyperAgentClient:
|
|
|
379
379
|
headers={"Authorization": "Bearer sk-hyper-test"},
|
|
380
380
|
json={"code": "promo-123"},
|
|
381
381
|
)
|
|
382
|
+
|
|
383
|
+
def test_redeem_grant_code_can_request_extension(self, mock_http):
|
|
384
|
+
mock_http._session.post.return_value.json.return_value = {
|
|
385
|
+
"grant": {"id": "grant-1", "code": "promo-123"},
|
|
386
|
+
"entitlement": {"id": "ent-1", "plan_id": "basic"},
|
|
387
|
+
}
|
|
388
|
+
mock_http._session.post.return_value.raise_for_status = Mock()
|
|
389
|
+
|
|
390
|
+
agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
|
|
391
|
+
agent.redeem_grant_code("promo-123", extend_existing=True)
|
|
392
|
+
|
|
393
|
+
mock_http._session.post.assert_called_with(
|
|
394
|
+
"https://api.hypercli.com/agents/billing/grants/redeem",
|
|
395
|
+
headers={"Authorization": "Bearer sk-hyper-test"},
|
|
396
|
+
json={"code": "promo-123", "extend_existing": True},
|
|
397
|
+
)
|
|
382
398
|
|
|
383
399
|
def test_openai_client_creation(self, mock_http):
|
|
384
400
|
"""Test that OpenAI client is created with correct config."""
|
|
@@ -118,6 +118,8 @@ def test_user_auth_me_returns_capabilities():
|
|
|
118
118
|
"email": "user@example.com",
|
|
119
119
|
"auth_type": "orchestra_key",
|
|
120
120
|
"capabilities": ["models:*", "voice:*"],
|
|
121
|
+
"tags": ["runtime=agent", "runtime_agent=agent-123"],
|
|
122
|
+
"runtime": {"kind": "agent", "agent_id": "agent-123"},
|
|
121
123
|
"has_active_subscription": True,
|
|
122
124
|
"key_id": "key-123",
|
|
123
125
|
"key_name": "runtime-key",
|
|
@@ -127,5 +129,8 @@ def test_user_auth_me_returns_capabilities():
|
|
|
127
129
|
|
|
128
130
|
assert auth_me.user_id == "user-123"
|
|
129
131
|
assert auth_me.capabilities == ["models:*", "voice:*"]
|
|
132
|
+
assert auth_me.tags == ["runtime=agent", "runtime_agent=agent-123"]
|
|
133
|
+
assert auth_me.is_runtime_agent is True
|
|
134
|
+
assert auth_me.runtime_agent_id == "agent-123"
|
|
130
135
|
assert auth_me.has_active_subscription is True
|
|
131
136
|
assert auth_me.key_id == "key-123"
|
|
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
|
|
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
|