cdx-manager 0.6.5 → 0.7.1
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.
- package/README.md +47 -7
- package/changelogs/CHANGELOGS_0_7_0.md +49 -0
- package/changelogs/CHANGELOGS_0_7_1.md +41 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_usage.py +11 -2
- package/src/cli.py +15 -4
- package/src/cli_commands.py +429 -5
- package/src/cli_render.py +3 -1
- package/src/provider_runtime.py +223 -11
- package/src/run_usage.py +133 -0
- package/src/session_service.py +35 -8
package/src/provider_runtime.py
CHANGED
|
@@ -6,6 +6,7 @@ import shlex
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
import uuid
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
|
|
11
12
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
@@ -13,6 +14,7 @@ from .errors import CdxError
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
17
|
+
REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
16
18
|
LAUNCH_PERMISSION_ARGS = {
|
|
17
19
|
PROVIDER_CLAUDE: {
|
|
18
20
|
"review": ["--permission-mode", "plan"],
|
|
@@ -27,6 +29,12 @@ LAUNCH_PERMISSION_ARGS = {
|
|
|
27
29
|
"full": ["-s", "danger-full-access", "-a", "never"],
|
|
28
30
|
},
|
|
29
31
|
}
|
|
32
|
+
HEADLESS_CODEX_PERMISSION_ARGS = {
|
|
33
|
+
"review": ["-s", "read-only"],
|
|
34
|
+
"default": ["-s", "workspace-write"],
|
|
35
|
+
"auto": ["-s", "workspace-write", "-c", 'approval_policy="never"'],
|
|
36
|
+
"full": ["--dangerously-bypass-approvals-and-sandbox"],
|
|
37
|
+
}
|
|
30
38
|
|
|
31
39
|
|
|
32
40
|
def _home_env_overrides(auth_home):
|
|
@@ -158,7 +166,7 @@ def _build_launch_transcript_path(session):
|
|
|
158
166
|
|
|
159
167
|
def _launch_power(session):
|
|
160
168
|
launch = session.get("launch") or {}
|
|
161
|
-
power = launch.get("power")
|
|
169
|
+
power = launch.get("reasoning_effort") or launch.get("reasoningEffort") or launch.get("power")
|
|
162
170
|
if power:
|
|
163
171
|
return power
|
|
164
172
|
if launch.get("fast") is True:
|
|
@@ -166,6 +174,28 @@ def _launch_power(session):
|
|
|
166
174
|
return None
|
|
167
175
|
|
|
168
176
|
|
|
177
|
+
def _normalize_reasoning_effort(reasoning_effort=None, power=None, usage="Unsupported reasoning effort."):
|
|
178
|
+
effort = str(reasoning_effort).strip().lower() if reasoning_effort is not None else None
|
|
179
|
+
alias = str(power).strip().lower() if power is not None else None
|
|
180
|
+
if effort == "":
|
|
181
|
+
raise CdxError(usage)
|
|
182
|
+
if alias == "":
|
|
183
|
+
raise CdxError(usage)
|
|
184
|
+
if effort and effort not in REASONING_EFFORT_VALUES:
|
|
185
|
+
raise CdxError(f"Unsupported reasoning effort: {reasoning_effort}")
|
|
186
|
+
if alias and alias not in REASONING_EFFORT_VALUES:
|
|
187
|
+
raise CdxError(f"Unsupported power: {power}")
|
|
188
|
+
if effort and alias and effort != alias:
|
|
189
|
+
raise CdxError("--reasoning-effort and --power must match when both are provided.")
|
|
190
|
+
resolved = effort or alias
|
|
191
|
+
if not resolved:
|
|
192
|
+
return {}
|
|
193
|
+
return {
|
|
194
|
+
"reasoning_effort": resolved,
|
|
195
|
+
"power": resolved,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
169
199
|
def _launch_config_args(session):
|
|
170
200
|
launch = session.get("launch") or {}
|
|
171
201
|
args = []
|
|
@@ -254,12 +284,8 @@ def _default_script_args(transcript_path, spec):
|
|
|
254
284
|
return ["-q", "-F", transcript_path, spec["command"]] + spec["args"]
|
|
255
285
|
|
|
256
286
|
|
|
257
|
-
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
258
|
-
|
|
259
|
-
if not isinstance(initial_prompt, str):
|
|
260
|
-
raise CdxError("initial_prompt must be a string.")
|
|
261
|
-
if len(initial_prompt) > 32768:
|
|
262
|
-
raise CdxError("initial_prompt exceeds maximum allowed length.")
|
|
287
|
+
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None, capture_transcript=True):
|
|
288
|
+
_validate_initial_prompt(initial_prompt)
|
|
263
289
|
cwd = cwd or os.getcwd()
|
|
264
290
|
env_override = env_override or {}
|
|
265
291
|
env = {**os.environ, **env_override}
|
|
@@ -280,7 +306,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
280
306
|
"env": claude_env,
|
|
281
307
|
},
|
|
282
308
|
"label": "claude",
|
|
283
|
-
}, env=env)
|
|
309
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
284
310
|
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
285
311
|
args = _launch_config_args(session)
|
|
286
312
|
if initial_prompt:
|
|
@@ -293,7 +319,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
293
319
|
"env": {**env, **_antigravity_env_overrides(_get_auth_home(session))},
|
|
294
320
|
},
|
|
295
321
|
"label": "antigravity",
|
|
296
|
-
}, env=env)
|
|
322
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
297
323
|
if session["provider"] == PROVIDER_OLLAMA:
|
|
298
324
|
launch = session.get("launch") or {}
|
|
299
325
|
model = launch.get("model") or session["name"]
|
|
@@ -309,7 +335,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
309
335
|
"env": ollama_env,
|
|
310
336
|
},
|
|
311
337
|
"label": "ollama",
|
|
312
|
-
}, env=env)
|
|
338
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
313
339
|
args = ["--no-alt-screen", "--cd", cwd] + _launch_config_args(session)
|
|
314
340
|
if initial_prompt:
|
|
315
341
|
args.append(initial_prompt)
|
|
@@ -320,7 +346,193 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
320
346
|
"env": {**env, "CODEX_HOME": _get_auth_home(session)},
|
|
321
347
|
},
|
|
322
348
|
"label": "codex",
|
|
323
|
-
}, env=env)
|
|
349
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _validate_initial_prompt(initial_prompt):
|
|
353
|
+
if initial_prompt is not None:
|
|
354
|
+
if not isinstance(initial_prompt, str):
|
|
355
|
+
raise CdxError("initial_prompt must be a string.")
|
|
356
|
+
if len(initial_prompt) > 32768:
|
|
357
|
+
raise CdxError("initial_prompt exceeds maximum allowed length.")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
361
|
+
_validate_initial_prompt(initial_prompt)
|
|
362
|
+
cwd = cwd or os.getcwd()
|
|
363
|
+
env = {**os.environ, **(env_override or {})}
|
|
364
|
+
launch = session.get("launch") or {}
|
|
365
|
+
power = _launch_power(session)
|
|
366
|
+
permission = launch.get("permission")
|
|
367
|
+
model = launch.get("model")
|
|
368
|
+
|
|
369
|
+
if session["provider"] == PROVIDER_CLAUDE:
|
|
370
|
+
args = ["--print", "--output-format", "json", "--name", session["name"]]
|
|
371
|
+
if model:
|
|
372
|
+
args += ["--model", model]
|
|
373
|
+
args += _launch_config_args(session)
|
|
374
|
+
if initial_prompt:
|
|
375
|
+
args.append(initial_prompt)
|
|
376
|
+
auth_home = _get_auth_home(session)
|
|
377
|
+
claude_env = _claude_env(env, auth_home)
|
|
378
|
+
oauth_token = _read_anthropic_oauth_token(auth_home)
|
|
379
|
+
if oauth_token:
|
|
380
|
+
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
381
|
+
return {
|
|
382
|
+
"command": "claude",
|
|
383
|
+
"args": args,
|
|
384
|
+
"options": {"cwd": cwd, "env": claude_env},
|
|
385
|
+
"label": "claude",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if session["provider"] == PROVIDER_CODEX:
|
|
389
|
+
args = ["exec", "--json", "-C", cwd]
|
|
390
|
+
if model:
|
|
391
|
+
args += ["-m", model]
|
|
392
|
+
if power:
|
|
393
|
+
args += ["-c", f'model_reasoning_effort="{power}"']
|
|
394
|
+
if permission:
|
|
395
|
+
args += HEADLESS_CODEX_PERMISSION_ARGS.get(permission, [])
|
|
396
|
+
if initial_prompt:
|
|
397
|
+
args.append(initial_prompt)
|
|
398
|
+
return {
|
|
399
|
+
"command": "codex",
|
|
400
|
+
"args": args,
|
|
401
|
+
"options": {"cwd": cwd, "env": {**env, "CODEX_HOME": _get_auth_home(session)}},
|
|
402
|
+
"label": "codex",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return _build_launch_spec(
|
|
406
|
+
session,
|
|
407
|
+
cwd=cwd,
|
|
408
|
+
env_override=env_override,
|
|
409
|
+
initial_prompt=initial_prompt,
|
|
410
|
+
capture_transcript=False,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _headless_artifact_paths(session, run_id=None):
|
|
415
|
+
run_id = run_id or uuid.uuid4().hex
|
|
416
|
+
log_dir = _get_launch_transcript_dir(session)
|
|
417
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
418
|
+
prefix = os.path.join(log_dir, f"cdx-run-{run_id}")
|
|
419
|
+
return {
|
|
420
|
+
"run_id": run_id,
|
|
421
|
+
"transcript_path": os.path.abspath(f"{prefix}.log"),
|
|
422
|
+
"stdout_path": os.path.abspath(f"{prefix}.stdout.log"),
|
|
423
|
+
"stderr_path": os.path.abspath(f"{prefix}.stderr.log"),
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _combine_headless_transcript(paths):
|
|
428
|
+
transcript_path = paths["transcript_path"]
|
|
429
|
+
with open(transcript_path, "w", encoding="utf-8", errors="replace") as transcript:
|
|
430
|
+
for label, path_key in (("stdout", "stdout_path"), ("stderr", "stderr_path")):
|
|
431
|
+
path = paths.get(path_key)
|
|
432
|
+
if not path:
|
|
433
|
+
continue
|
|
434
|
+
transcript.write(f"--- {label} ---\n")
|
|
435
|
+
try:
|
|
436
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
437
|
+
content = handle.read()
|
|
438
|
+
except OSError:
|
|
439
|
+
content = ""
|
|
440
|
+
transcript.write(content)
|
|
441
|
+
if content and not content.endswith("\n"):
|
|
442
|
+
transcript.write("\n")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _headless_run_info(paths, spec, start_time, returncode):
|
|
446
|
+
end_time = datetime.now(timezone.utc)
|
|
447
|
+
return {
|
|
448
|
+
**paths,
|
|
449
|
+
"started_at": start_time.isoformat().replace("+00:00", "Z"),
|
|
450
|
+
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
451
|
+
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
452
|
+
"command": spec.get("command"),
|
|
453
|
+
"args": list(spec.get("args") or []),
|
|
454
|
+
"label": spec.get("label"),
|
|
455
|
+
"pid": None,
|
|
456
|
+
"returncode": returncode,
|
|
457
|
+
"timed_out": False,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
462
|
+
timeout_seconds=None, spawn=None, run_id=None):
|
|
463
|
+
spawn = spawn or subprocess.Popen
|
|
464
|
+
spec = _build_headless_launch_spec(
|
|
465
|
+
session,
|
|
466
|
+
cwd=cwd,
|
|
467
|
+
env_override=env_override,
|
|
468
|
+
initial_prompt=initial_prompt,
|
|
469
|
+
)
|
|
470
|
+
paths = _headless_artifact_paths(session, run_id=run_id)
|
|
471
|
+
start_time = datetime.now(timezone.utc)
|
|
472
|
+
command = spec["command"]
|
|
473
|
+
if spawn is subprocess.Popen:
|
|
474
|
+
command = _resolve_command(command, spec.get("options", {}).get("env"))
|
|
475
|
+
|
|
476
|
+
child = None
|
|
477
|
+
timed_out = False
|
|
478
|
+
with open(paths["stdout_path"], "w", encoding="utf-8", errors="replace") as stdout_file, \
|
|
479
|
+
open(paths["stderr_path"], "w", encoding="utf-8", errors="replace") as stderr_file:
|
|
480
|
+
try:
|
|
481
|
+
child = spawn(
|
|
482
|
+
[command] + spec["args"],
|
|
483
|
+
stdout=stdout_file,
|
|
484
|
+
stderr=stderr_file,
|
|
485
|
+
**{k: v for k, v in spec.get("options", {}).items() if k not in ("stdio", "stdout", "stderr")},
|
|
486
|
+
)
|
|
487
|
+
except FileNotFoundError as error:
|
|
488
|
+
_combine_headless_transcript(paths)
|
|
489
|
+
cdx_error = CdxError(f"{spec['label']} CLI not found on PATH: {spec['command']}", 127)
|
|
490
|
+
cdx_error.run_info = _headless_run_info(paths, spec, start_time, 127)
|
|
491
|
+
raise cdx_error from error
|
|
492
|
+
except OSError as error:
|
|
493
|
+
_combine_headless_transcript(paths)
|
|
494
|
+
cdx_error = CdxError(f"Failed to start {spec['label']}: {error}", 126)
|
|
495
|
+
cdx_error.run_info = _headless_run_info(paths, spec, start_time, 126)
|
|
496
|
+
raise cdx_error from error
|
|
497
|
+
try:
|
|
498
|
+
if timeout_seconds is None:
|
|
499
|
+
child.wait()
|
|
500
|
+
else:
|
|
501
|
+
child.wait(timeout=timeout_seconds)
|
|
502
|
+
except TypeError:
|
|
503
|
+
child.wait()
|
|
504
|
+
except subprocess.TimeoutExpired:
|
|
505
|
+
timed_out = True
|
|
506
|
+
try:
|
|
507
|
+
child.terminate()
|
|
508
|
+
child.wait(timeout=5)
|
|
509
|
+
except Exception:
|
|
510
|
+
try:
|
|
511
|
+
child.kill()
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
try:
|
|
515
|
+
child.wait()
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
518
|
+
|
|
519
|
+
_combine_headless_transcript(paths)
|
|
520
|
+
end_time = datetime.now(timezone.utc)
|
|
521
|
+
returncode = getattr(child, "returncode", None) if child is not None else None
|
|
522
|
+
if timed_out and (returncode is None or returncode == 0):
|
|
523
|
+
returncode = 124
|
|
524
|
+
return {
|
|
525
|
+
**paths,
|
|
526
|
+
"started_at": start_time.isoformat().replace("+00:00", "Z"),
|
|
527
|
+
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
528
|
+
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
529
|
+
"command": spec.get("command"),
|
|
530
|
+
"args": list(spec.get("args") or []),
|
|
531
|
+
"label": spec.get("label"),
|
|
532
|
+
"pid": getattr(child, "pid", None),
|
|
533
|
+
"returncode": returncode,
|
|
534
|
+
"timed_out": timed_out,
|
|
535
|
+
}
|
|
324
536
|
|
|
325
537
|
|
|
326
538
|
def _build_login_status_spec(session, env_override=None):
|
package/src/run_usage.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
USAGE_KEYS = ("input_tokens", "output_tokens", "reasoning_tokens", "total_tokens")
|
|
5
|
+
SUPPORTED_PROVIDERS = {"claude", "codex"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def empty_usage():
|
|
9
|
+
return {key: None for key in USAGE_KEYS}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_run_usage(provider, stdout_path):
|
|
13
|
+
if not stdout_path or not provider:
|
|
14
|
+
return empty_usage()
|
|
15
|
+
if provider not in SUPPORTED_PROVIDERS:
|
|
16
|
+
return empty_usage()
|
|
17
|
+
try:
|
|
18
|
+
with open(stdout_path, "r", encoding="utf-8", errors="replace") as handle:
|
|
19
|
+
text = handle.read()
|
|
20
|
+
except OSError:
|
|
21
|
+
return empty_usage()
|
|
22
|
+
if not text.strip():
|
|
23
|
+
return empty_usage()
|
|
24
|
+
|
|
25
|
+
records = _parse_json_records(text)
|
|
26
|
+
if not records:
|
|
27
|
+
return empty_usage()
|
|
28
|
+
|
|
29
|
+
usage = _extract_usage_from_records(records)
|
|
30
|
+
if not _has_usage(usage):
|
|
31
|
+
return empty_usage()
|
|
32
|
+
return usage
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_json_records(text):
|
|
36
|
+
stripped = text.strip()
|
|
37
|
+
try:
|
|
38
|
+
return [json.loads(stripped)]
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
records = []
|
|
43
|
+
for line in stripped.splitlines():
|
|
44
|
+
line = line.strip()
|
|
45
|
+
if not line:
|
|
46
|
+
continue
|
|
47
|
+
try:
|
|
48
|
+
records.append(json.loads(line))
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
return []
|
|
51
|
+
return records
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_usage_from_records(records):
|
|
55
|
+
latest = None
|
|
56
|
+
for record in records:
|
|
57
|
+
candidate = _find_usage(record)
|
|
58
|
+
if _has_usage(candidate):
|
|
59
|
+
latest = candidate
|
|
60
|
+
return latest or empty_usage()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_usage(value):
|
|
64
|
+
if isinstance(value, dict):
|
|
65
|
+
direct = _usage_from_dict(value)
|
|
66
|
+
if _has_usage(direct):
|
|
67
|
+
return direct
|
|
68
|
+
for child in value.values():
|
|
69
|
+
found = _find_usage(child)
|
|
70
|
+
if _has_usage(found):
|
|
71
|
+
return found
|
|
72
|
+
if isinstance(value, list):
|
|
73
|
+
for child in value:
|
|
74
|
+
found = _find_usage(child)
|
|
75
|
+
if _has_usage(found):
|
|
76
|
+
return found
|
|
77
|
+
return empty_usage()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _usage_from_dict(value):
|
|
81
|
+
usage = value.get("usage") if isinstance(value.get("usage"), dict) else value
|
|
82
|
+
if not isinstance(usage, dict):
|
|
83
|
+
return empty_usage()
|
|
84
|
+
|
|
85
|
+
input_tokens = _int_value(
|
|
86
|
+
usage.get("input_tokens"),
|
|
87
|
+
usage.get("prompt_tokens"),
|
|
88
|
+
usage.get("cache_creation_input_tokens"),
|
|
89
|
+
usage.get("cache_read_input_tokens"),
|
|
90
|
+
)
|
|
91
|
+
output_tokens = _int_value(usage.get("output_tokens"), usage.get("completion_tokens"))
|
|
92
|
+
reasoning_tokens = _int_value(
|
|
93
|
+
usage.get("reasoning_tokens"),
|
|
94
|
+
_nested_int(usage, "output_tokens_details", "reasoning_tokens"),
|
|
95
|
+
_nested_int(usage, "completion_tokens_details", "reasoning_tokens"),
|
|
96
|
+
)
|
|
97
|
+
total_tokens = _first_int(usage.get("total_tokens"))
|
|
98
|
+
if total_tokens is None and input_tokens is not None and output_tokens is not None:
|
|
99
|
+
total_tokens = input_tokens + output_tokens
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"input_tokens": input_tokens,
|
|
103
|
+
"output_tokens": output_tokens,
|
|
104
|
+
"reasoning_tokens": reasoning_tokens,
|
|
105
|
+
"total_tokens": total_tokens,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _nested_int(value, parent, child):
|
|
110
|
+
nested = value.get(parent)
|
|
111
|
+
if not isinstance(nested, dict):
|
|
112
|
+
return None
|
|
113
|
+
return nested.get(child)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _first_int(value):
|
|
117
|
+
try:
|
|
118
|
+
parsed = int(value)
|
|
119
|
+
except (TypeError, ValueError):
|
|
120
|
+
return None
|
|
121
|
+
return parsed if parsed >= 0 else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _int_value(*values):
|
|
125
|
+
parsed = [_first_int(value) for value in values]
|
|
126
|
+
parsed = [value for value in parsed if value is not None]
|
|
127
|
+
if not parsed:
|
|
128
|
+
return None
|
|
129
|
+
return sum(parsed)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_usage(usage):
|
|
133
|
+
return isinstance(usage, dict) and any(usage.get(key) is not None for key in USAGE_KEYS)
|
package/src/session_service.py
CHANGED
|
@@ -40,6 +40,8 @@ RESERVED_SESSION_NAMES = {
|
|
|
40
40
|
"ren",
|
|
41
41
|
"rename",
|
|
42
42
|
"rmv",
|
|
43
|
+
"run",
|
|
44
|
+
"select",
|
|
43
45
|
"config",
|
|
44
46
|
"set",
|
|
45
47
|
"status",
|
|
@@ -54,8 +56,11 @@ RESERVED_SESSION_NAMES = {
|
|
|
54
56
|
STATUS_CACHE_TTL_SECONDS = 60
|
|
55
57
|
CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
|
|
56
58
|
LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
|
|
59
|
+
LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
57
60
|
LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
|
|
58
61
|
MAX_LAUNCH_MODEL_LENGTH = 128
|
|
62
|
+
MIN_LAUNCH_PRIORITY = 0
|
|
63
|
+
MAX_LAUNCH_PRIORITY = 100
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
def _encode(name):
|
|
@@ -96,6 +101,11 @@ def _normalize_launch_settings(settings):
|
|
|
96
101
|
if power not in LAUNCH_POWER_VALUES:
|
|
97
102
|
raise CdxError(f"Unsupported power: {settings['power']}")
|
|
98
103
|
normalized["power"] = power
|
|
104
|
+
if "reasoning_effort" in settings and settings["reasoning_effort"] is not None:
|
|
105
|
+
effort = str(settings["reasoning_effort"]).strip().lower()
|
|
106
|
+
if effort not in LAUNCH_REASONING_EFFORT_VALUES:
|
|
107
|
+
raise CdxError(f"Unsupported reasoning effort: {settings['reasoning_effort']}")
|
|
108
|
+
normalized["reasoning_effort"] = effort
|
|
99
109
|
if "permission" in settings and settings["permission"] is not None:
|
|
100
110
|
permission = str(settings["permission"]).strip().lower()
|
|
101
111
|
if permission not in LAUNCH_PERMISSION_VALUES:
|
|
@@ -120,6 +130,14 @@ def _normalize_launch_settings(settings):
|
|
|
120
130
|
if len(model) > MAX_LAUNCH_MODEL_LENGTH or any(ord(ch) < 32 or ord(ch) == 127 for ch in model):
|
|
121
131
|
raise CdxError("Model contains unsupported characters.")
|
|
122
132
|
normalized["model"] = model
|
|
133
|
+
if "priority" in settings and settings["priority"] is not None:
|
|
134
|
+
try:
|
|
135
|
+
priority = int(settings["priority"])
|
|
136
|
+
except (TypeError, ValueError) as error:
|
|
137
|
+
raise CdxError(f"Unsupported priority: {settings['priority']}") from error
|
|
138
|
+
if priority < MIN_LAUNCH_PRIORITY or priority > MAX_LAUNCH_PRIORITY:
|
|
139
|
+
raise CdxError(f"Unsupported priority: {settings['priority']}")
|
|
140
|
+
normalized["priority"] = priority
|
|
123
141
|
return normalized
|
|
124
142
|
|
|
125
143
|
|
|
@@ -171,14 +189,23 @@ def _to_local_iso(value):
|
|
|
171
189
|
return parsed.astimezone().isoformat()
|
|
172
190
|
|
|
173
191
|
|
|
192
|
+
def _normalize_pct_value(value):
|
|
193
|
+
if value is None:
|
|
194
|
+
return None
|
|
195
|
+
try:
|
|
196
|
+
return max(0, min(100, round(float(value))))
|
|
197
|
+
except (TypeError, ValueError):
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
174
201
|
def _normalize_status_payload(payload=None):
|
|
175
202
|
if payload is None:
|
|
176
203
|
payload = {}
|
|
177
204
|
now = _local_now_iso()
|
|
178
205
|
return {
|
|
179
|
-
"usage_pct": payload.get("usage_pct"),
|
|
180
|
-
"remaining_5h_pct": payload.get("remaining_5h_pct"),
|
|
181
|
-
"remaining_week_pct": payload.get("remaining_week_pct"),
|
|
206
|
+
"usage_pct": _normalize_pct_value(payload.get("usage_pct")),
|
|
207
|
+
"remaining_5h_pct": _normalize_pct_value(payload.get("remaining_5h_pct")),
|
|
208
|
+
"remaining_week_pct": _normalize_pct_value(payload.get("remaining_week_pct")),
|
|
182
209
|
"credits": payload.get("credits"),
|
|
183
210
|
"reset_5h_at": payload.get("reset_5h_at"),
|
|
184
211
|
"reset_week_at": payload.get("reset_week_at"),
|
|
@@ -279,8 +306,8 @@ def _compute_available_pct(status):
|
|
|
279
306
|
if not status:
|
|
280
307
|
return None
|
|
281
308
|
values = [
|
|
282
|
-
status.get("remaining_5h_pct"),
|
|
283
|
-
status.get("remaining_week_pct"),
|
|
309
|
+
_normalize_pct_value(status.get("remaining_5h_pct")),
|
|
310
|
+
_normalize_pct_value(status.get("remaining_week_pct")),
|
|
284
311
|
]
|
|
285
312
|
values = [value for value in values if value is not None]
|
|
286
313
|
if not values:
|
|
@@ -773,7 +800,7 @@ def create_session_service(options=None):
|
|
|
773
800
|
raise CdxError(f"Unknown session: {name}")
|
|
774
801
|
if not keys:
|
|
775
802
|
raise CdxError("At least one launch setting is required.")
|
|
776
|
-
allowed = {"power", "permission", "fast", "model"}
|
|
803
|
+
allowed = {"power", "permission", "fast", "model", "priority"}
|
|
777
804
|
unknown = [key for key in keys if key not in allowed]
|
|
778
805
|
if unknown:
|
|
779
806
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|
|
@@ -963,8 +990,8 @@ def create_session_service(options=None):
|
|
|
963
990
|
"status": "enabled" if enabled else "disabled",
|
|
964
991
|
"auth_status": (s.get("auth") or {}).get("status") or "unknown",
|
|
965
992
|
"auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
|
|
966
|
-
"remaining_5h_pct": row_status.get("remaining_5h_pct") if row_status else None,
|
|
967
|
-
"remaining_week_pct": row_status.get("remaining_week_pct") if row_status else None,
|
|
993
|
+
"remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
|
|
994
|
+
"remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
|
|
968
995
|
"credits": row_status.get("credits") if row_status else None,
|
|
969
996
|
"available_pct": _compute_available_pct(row_status),
|
|
970
997
|
"reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
|