claude-dev-env 8.45.1 → 8.46.0
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/package.json
CHANGED
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Mapping, Sequence
|
|
15
15
|
|
|
16
16
|
import account_broker_support as support
|
|
17
|
+
import codex_account_meters
|
|
17
18
|
from account_broker_support import (
|
|
18
19
|
all_product_adapters,
|
|
19
20
|
Account,
|
|
@@ -310,11 +311,14 @@ def run_job(
|
|
|
310
311
|
def _parser() -> argparse.ArgumentParser:
|
|
311
312
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
312
313
|
commands = parser.add_subparsers(dest="action", required=True)
|
|
313
|
-
for each_action in ("choose", "check", "accounts", "run"):
|
|
314
|
+
for each_action in ("choose", "check", "accounts", "limits", "run"):
|
|
314
315
|
command = commands.add_parser(each_action)
|
|
315
316
|
command.add_argument("--product", choices=[each_product.value for each_product in Product], required=True)
|
|
316
317
|
if each_action == "choose":
|
|
317
318
|
command.add_argument("--spent", action="append", default=[])
|
|
319
|
+
if each_action == "limits":
|
|
320
|
+
command.add_argument("--home", type=Path, required=True)
|
|
321
|
+
command.add_argument("--codex", type=Path)
|
|
318
322
|
if each_action == "run":
|
|
319
323
|
command.add_argument("--report", type=Path, required=True)
|
|
320
324
|
command.add_argument("command", nargs=argparse.REMAINDER)
|
|
@@ -344,6 +348,19 @@ def _accounts_cli(product: Product) -> int:
|
|
|
344
348
|
return 0
|
|
345
349
|
|
|
346
350
|
|
|
351
|
+
def _limits_cli(product: Product, parsed: argparse.Namespace) -> int:
|
|
352
|
+
if product is not Product.CODEX:
|
|
353
|
+
raise BrokerConfigurationError("limits reads Codex accounts only")
|
|
354
|
+
try:
|
|
355
|
+
rate_limit_records = codex_account_meters.read_rate_limit_records(
|
|
356
|
+
codex_account_meters.resolve_codex_path(parsed.codex), parsed.home
|
|
357
|
+
)
|
|
358
|
+
except (codex_account_meters.CodexMeterUnreadError, OSError) as error:
|
|
359
|
+
raise BrokerConfigurationError(str(error)) from error
|
|
360
|
+
print(json.dumps({"home": str(parsed.home), "rate_limits": rate_limit_records}))
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
347
364
|
def _run_cli(product: Product, parsed: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
348
365
|
command = list(parsed.command)
|
|
349
366
|
if command and command[0] == "--":
|
|
@@ -387,6 +404,8 @@ def main(all_arguments: Sequence[str]) -> int:
|
|
|
387
404
|
try:
|
|
388
405
|
if parsed.action == "accounts":
|
|
389
406
|
return _accounts_cli(product)
|
|
407
|
+
if parsed.action == "limits":
|
|
408
|
+
return _limits_cli(product, parsed)
|
|
390
409
|
if parsed.action == "run":
|
|
391
410
|
return _run_cli(product, parsed, parser)
|
|
392
411
|
return _choose_cli(product, parsed)
|
|
@@ -161,7 +161,7 @@ def _parse_window(raw_window: object) -> UsageWindow | None:
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
def
|
|
164
|
+
def _reply_body(reply_text: str) -> Mapping[str, object]:
|
|
165
165
|
try:
|
|
166
166
|
reply = json.loads(reply_text)
|
|
167
167
|
except json.JSONDecodeError as error:
|
|
@@ -171,7 +171,13 @@ def _rate_limits_mapping(reply_text: str) -> Mapping[str, object]:
|
|
|
171
171
|
if reply.get("error") is not None:
|
|
172
172
|
raise CodexMeterUnreadError(f"rate-limit read failed: {reply['error']}")
|
|
173
173
|
body = reply.get("result")
|
|
174
|
-
|
|
174
|
+
if not isinstance(body, dict):
|
|
175
|
+
raise CodexMeterUnreadError("rate-limit reply has no result")
|
|
176
|
+
return body
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _rate_limits_mapping(reply_text: str) -> Mapping[str, object]:
|
|
180
|
+
rate_limits = _reply_body(reply_text).get("rateLimits")
|
|
175
181
|
if not isinstance(rate_limits, dict):
|
|
176
182
|
raise CodexMeterUnreadError("rate-limit reply names no rateLimits")
|
|
177
183
|
return rate_limits
|
|
@@ -338,13 +344,45 @@ def read_codex_meters(
|
|
|
338
344
|
Raises:
|
|
339
345
|
CodexMeterUnreadError: The server failed, timed out, or sent no usable reply.
|
|
340
346
|
"""
|
|
347
|
+
return parse_rate_limits_reply(_rate_limits_reply_line(codex_path, codex_home, exchange))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def read_rate_limit_records(
|
|
351
|
+
codex_path: Path,
|
|
352
|
+
codex_home: Path,
|
|
353
|
+
exchange: ServerExchange = exchange_with_app_server,
|
|
354
|
+
) -> Mapping[str, object]:
|
|
355
|
+
"""Read one account's whole rate-limit reply through the app server.
|
|
356
|
+
|
|
357
|
+
::
|
|
358
|
+
|
|
359
|
+
{"id": 2, "result": {"rateLimits": {...}, "rateLimitsByLimitId": {...}}}
|
|
360
|
+
-> {"rateLimits": {...}, "rateLimitsByLimitId": {...}}
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
codex_path: The Codex executable.
|
|
364
|
+
codex_home: The account's Codex home.
|
|
365
|
+
exchange: Sends messages and returns output lines; tests pass a fake.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
The reply's ``result`` object, with every limit record it carries.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
CodexMeterUnreadError: The server failed, timed out, or sent no usable reply.
|
|
372
|
+
"""
|
|
373
|
+
return _reply_body(_rate_limits_reply_line(codex_path, codex_home, exchange))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _rate_limits_reply_line(
|
|
377
|
+
codex_path: Path, codex_home: Path, exchange: ServerExchange
|
|
378
|
+
) -> str:
|
|
341
379
|
try:
|
|
342
380
|
all_lines = exchange(codex_path, codex_home, request_messages())
|
|
343
381
|
except (OSError, subprocess.SubprocessError, UnicodeDecodeError) as error:
|
|
344
382
|
raise CodexMeterUnreadError(f"codex app-server failed: {error}") from error
|
|
345
383
|
for each_line in all_lines:
|
|
346
384
|
if _is_rate_limits_reply(each_line):
|
|
347
|
-
return
|
|
385
|
+
return each_line
|
|
348
386
|
raise CodexMeterUnreadError("codex app-server sent no rate-limit reply")
|
|
349
387
|
|
|
350
388
|
|
|
@@ -663,3 +663,41 @@ def test_should_run_job_through_override(
|
|
|
663
663
|
assert outcome.status == "served"
|
|
664
664
|
assert outcome.attempts == (("only", "served"),)
|
|
665
665
|
assert calls == [b"input"]
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_should_print_the_whole_rate_limit_result_for_one_home(
|
|
670
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
671
|
+
) -> None:
|
|
672
|
+
rate_limit_records = {"rateLimitsByLimitId": {"codex": {"primary": {"usedPercent": 20}}}}
|
|
673
|
+
observed: list[tuple[Path, Path]] = []
|
|
674
|
+
|
|
675
|
+
def read_result(codex_path: Path, codex_home: Path) -> dict[str, object]:
|
|
676
|
+
observed.append((codex_path, codex_home))
|
|
677
|
+
return rate_limit_records
|
|
678
|
+
|
|
679
|
+
monkeypatch.setattr(account_broker.codex_account_meters, "read_rate_limit_records", read_result)
|
|
680
|
+
home = tmp_path / "codex-2"
|
|
681
|
+
|
|
682
|
+
assert account_broker.main(("limits", "--product", "codex", "--home", str(home), "--codex", "codex-bin")) == 0
|
|
683
|
+
assert json.loads(capsys.readouterr().out) == {"home": str(home), "rate_limits": rate_limit_records}
|
|
684
|
+
assert observed == [(Path("codex-bin"), home)]
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def test_should_exit_two_when_the_limits_read_fails(
|
|
688
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
689
|
+
) -> None:
|
|
690
|
+
def read_result(_codex_path: Path, _codex_home: Path) -> dict[str, object]:
|
|
691
|
+
raise account_broker.codex_account_meters.CodexMeterUnreadError("codex app-server sent no rate-limit reply")
|
|
692
|
+
|
|
693
|
+
monkeypatch.setattr(account_broker.codex_account_meters, "read_rate_limit_records", read_result)
|
|
694
|
+
|
|
695
|
+
assert account_broker.main(("limits", "--product", "codex", "--home", str(tmp_path), "--codex", "codex-bin")) == 2
|
|
696
|
+
captured = capsys.readouterr()
|
|
697
|
+
assert captured.out == ""
|
|
698
|
+
assert "no rate-limit reply" in captured.err
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def test_should_refuse_limits_for_claude(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
702
|
+
assert account_broker.main(("limits", "--product", "claude", "--home", str(tmp_path))) == 2
|
|
703
|
+
assert "Codex accounts only" in capsys.readouterr().err
|
|
@@ -15,6 +15,7 @@ from codex_account_meters import (
|
|
|
15
15
|
exchange_with_app_server,
|
|
16
16
|
parse_rate_limits_reply,
|
|
17
17
|
read_codex_meters,
|
|
18
|
+
read_rate_limit_records,
|
|
18
19
|
request_messages,
|
|
19
20
|
resolve_codex_path,
|
|
20
21
|
)
|
|
@@ -163,6 +164,38 @@ class TestReadCodexMeters:
|
|
|
163
164
|
read_codex_meters(Path("codex"), Path("/h"), broken_exchange)
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
|
|
168
|
+
class TestReadRateLimitRecords:
|
|
169
|
+
def should_return_every_limit_record_the_reply_carries(self) -> None:
|
|
170
|
+
rate_limit_records = {
|
|
171
|
+
"rateLimits": {"limitId": "codex", "primary": {"usedPercent": 20}},
|
|
172
|
+
"rateLimitsByLimitId": {
|
|
173
|
+
"codex": {"limitId": "codex", "planType": "pro"},
|
|
174
|
+
"codex_other": {"limitId": "codex_other"},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
assert (
|
|
179
|
+
read_rate_limit_records(
|
|
180
|
+
Path("codex"),
|
|
181
|
+
Path("/h"),
|
|
182
|
+
lambda *_: [json.dumps({"id": 1}), json.dumps({"id": 2, "result": rate_limit_records})],
|
|
183
|
+
)
|
|
184
|
+
== rate_limit_records
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def should_raise_unread_on_an_error_reply(self) -> None:
|
|
188
|
+
with pytest.raises(CodexMeterUnreadError, match="read failed"):
|
|
189
|
+
read_rate_limit_records(
|
|
190
|
+
Path("codex"),
|
|
191
|
+
Path("/h"),
|
|
192
|
+
lambda *_: [json.dumps({"id": 2, "error": {"code": 1}})],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def should_raise_unread_when_the_server_never_answers(self) -> None:
|
|
196
|
+
with pytest.raises(CodexMeterUnreadError, match="no rate-limit reply"):
|
|
197
|
+
read_rate_limit_records(Path("codex"), Path("/h"), lambda *_: [])
|
|
198
|
+
|
|
166
199
|
FAKE_APP_SERVER = """#!/usr/bin/env python3
|
|
167
200
|
import json, os, sys
|
|
168
201
|
for line in sys.stdin:
|