agentpool-cli 0.1.0__py3-none-any.whl
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.
- agentpool/__init__.py +3 -0
- agentpool/agent_io.py +134 -0
- agentpool/artifacts.py +151 -0
- agentpool/cli.py +1199 -0
- agentpool/config.py +373 -0
- agentpool/docs/agentpool-skill.md +85 -0
- agentpool/docs/onboarding.md +169 -0
- agentpool/event_detection.py +150 -0
- agentpool/fixtures/__init__.py +1 -0
- agentpool/fixtures/fake_agents/__init__.py +1 -0
- agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_common.py +44 -0
- agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
- agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
- agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
- agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
- agentpool/git_worktree.py +144 -0
- agentpool/mcp/__init__.py +1 -0
- agentpool/mcp/resources.py +64 -0
- agentpool/mcp/tools.py +259 -0
- agentpool/mcp_server.py +487 -0
- agentpool/models.py +310 -0
- agentpool/onboarding.py +1279 -0
- agentpool/policy.py +63 -0
- agentpool/provider_model_catalog.json +997 -0
- agentpool/providers/__init__.py +3 -0
- agentpool/providers/base.py +411 -0
- agentpool/providers/registry.py +139 -0
- agentpool/redaction.py +30 -0
- agentpool/runtimes/__init__.py +3 -0
- agentpool/runtimes/base.py +36 -0
- agentpool/runtimes/tmux.py +133 -0
- agentpool/session_manager.py +1061 -0
- agentpool/stats/__init__.py +6 -0
- agentpool/stats/card.py +74 -0
- agentpool/stats/compute.py +496 -0
- agentpool/stats/queries.py +138 -0
- agentpool/stats/render.py +103 -0
- agentpool/stats/window.py +85 -0
- agentpool/store.py +478 -0
- agentpool/usage/__init__.py +1 -0
- agentpool/usage/_common.py +223 -0
- agentpool/usage/ccusage.py +130 -0
- agentpool/usage/claude.py +23 -0
- agentpool/usage/codex.py +210 -0
- agentpool/usage/codexbar.py +186 -0
- agentpool/usage/combine.py +71 -0
- agentpool/usage/copilot.py +146 -0
- agentpool/usage/devin.py +265 -0
- agentpool/usage/parsers.py +41 -0
- agentpool/usage/probes.py +52 -0
- agentpool/usage/provider_parsers.py +276 -0
- agentpool/usage/summary.py +166 -0
- agentpool/utils.py +59 -0
- agentpool_cli-0.1.0.dist-info/METADATA +292 -0
- agentpool_cli-0.1.0.dist-info/RECORD +60 -0
- agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
- agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agentpool.models import CapacitySnapshot, Confidence, UsageStatus, UsageWindow, UsageWindowKind
|
|
10
|
+
from agentpool.usage._common import (
|
|
11
|
+
ProbeError,
|
|
12
|
+
_clamp_percent,
|
|
13
|
+
_clean_optional_string,
|
|
14
|
+
_number,
|
|
15
|
+
_parse_datetime,
|
|
16
|
+
_request_json,
|
|
17
|
+
_status_from_windows,
|
|
18
|
+
unknown,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def copilot_cli_usage_snapshot(provider_id: str, binary: str | None = None) -> CapacitySnapshot:
|
|
23
|
+
_ = binary
|
|
24
|
+
token_result = _copilot_token()
|
|
25
|
+
if not token_result:
|
|
26
|
+
return CapacitySnapshot(
|
|
27
|
+
provider_id=provider_id,
|
|
28
|
+
status=UsageStatus.UNAUTHENTICATED,
|
|
29
|
+
confidence=Confidence.UNKNOWN,
|
|
30
|
+
warnings=[
|
|
31
|
+
"No Copilot API token found. Set AGENTPOOL_COPILOT_TOKEN/GITHUB_TOKEN/GH_TOKEN, "
|
|
32
|
+
"or authenticate gh so `gh auth token` can provide a token."
|
|
33
|
+
],
|
|
34
|
+
raw={"source": "github_copilot_internal_api"},
|
|
35
|
+
)
|
|
36
|
+
token, token_source = token_result
|
|
37
|
+
try:
|
|
38
|
+
request = urllib.request.Request(
|
|
39
|
+
"https://api.github.com/copilot_internal/user",
|
|
40
|
+
headers={
|
|
41
|
+
"Authorization": f"token {token}",
|
|
42
|
+
"Accept": "application/json",
|
|
43
|
+
# GitHub Copilot usage API client id; not an MCP host or VS Code IDE config.
|
|
44
|
+
"Editor-Version": "vscode/1.96.2",
|
|
45
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
46
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
47
|
+
"X-Github-Api-Version": "2025-04-01",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
payload = _request_json(request)
|
|
51
|
+
snapshot = parse_copilot_usage_response(provider_id, payload)
|
|
52
|
+
snapshot.raw["source"] = "github_copilot_internal_api"
|
|
53
|
+
snapshot.raw["token_source"] = token_source
|
|
54
|
+
return snapshot
|
|
55
|
+
except ProbeError as exc:
|
|
56
|
+
return unknown(
|
|
57
|
+
provider_id,
|
|
58
|
+
f"Copilot internal API probe failed: {exc}",
|
|
59
|
+
source="github_copilot_internal_api",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_copilot_usage_response(provider_id: str, payload: dict[str, Any]) -> CapacitySnapshot:
|
|
64
|
+
snapshots = payload.get("quota_snapshots")
|
|
65
|
+
if not isinstance(snapshots, dict):
|
|
66
|
+
snapshots = {}
|
|
67
|
+
windows = []
|
|
68
|
+
for name, item in (("premium_interactions", snapshots.get("premium_interactions")), ("chat", snapshots.get("chat"))):
|
|
69
|
+
window = _copilot_window(name, item)
|
|
70
|
+
if window:
|
|
71
|
+
windows.append(window)
|
|
72
|
+
if not windows:
|
|
73
|
+
windows = _copilot_windows_from_legacy_counts(payload)
|
|
74
|
+
if not windows:
|
|
75
|
+
raise ProbeError("Copilot response did not include usable quota snapshots.")
|
|
76
|
+
reset_at = _parse_datetime(payload.get("quota_reset_date"))
|
|
77
|
+
if reset_at:
|
|
78
|
+
windows = [window.model_copy(update={"reset_at": reset_at}) for window in windows]
|
|
79
|
+
return CapacitySnapshot(
|
|
80
|
+
provider_id=provider_id,
|
|
81
|
+
status=_status_from_windows(windows),
|
|
82
|
+
confidence=Confidence.OFFICIAL,
|
|
83
|
+
windows=windows,
|
|
84
|
+
reset_at=reset_at,
|
|
85
|
+
raw={"plan": _clean_optional_string(payload.get("copilot_plan"))},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _copilot_token() -> tuple[str, str] | None:
|
|
90
|
+
for key in ("AGENTPOOL_COPILOT_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"):
|
|
91
|
+
value = os.environ.get(key)
|
|
92
|
+
if value:
|
|
93
|
+
return value, key
|
|
94
|
+
gh = shutil.which("gh")
|
|
95
|
+
if not gh:
|
|
96
|
+
return None
|
|
97
|
+
proc = subprocess.run([gh, "auth", "token"], text=True, capture_output=True, timeout=5, check=False)
|
|
98
|
+
token = proc.stdout.strip()
|
|
99
|
+
if proc.returncode == 0 and token:
|
|
100
|
+
return token, "gh auth token"
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _copilot_window(name: str, item: object) -> UsageWindow | None:
|
|
105
|
+
if not isinstance(item, dict):
|
|
106
|
+
return None
|
|
107
|
+
percent_remaining = _number(item.get("percent_remaining"))
|
|
108
|
+
entitlement = _number(item.get("entitlement"))
|
|
109
|
+
remaining = _number(item.get("remaining"))
|
|
110
|
+
if percent_remaining is None and entitlement and remaining is not None:
|
|
111
|
+
percent_remaining = (remaining / entitlement) * 100.0
|
|
112
|
+
if percent_remaining is None or (entitlement == 0 and remaining == 0 and percent_remaining == 0):
|
|
113
|
+
return None
|
|
114
|
+
return UsageWindow(
|
|
115
|
+
name=name,
|
|
116
|
+
kind=UsageWindowKind.MONTHLY,
|
|
117
|
+
used_percent=_clamp_percent(100.0 - percent_remaining),
|
|
118
|
+
remaining_percent=_clamp_percent(percent_remaining),
|
|
119
|
+
remaining_units=remaining,
|
|
120
|
+
confidence=Confidence.OFFICIAL,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _copilot_windows_from_legacy_counts(payload: dict[str, Any]) -> list[UsageWindow]:
|
|
125
|
+
monthly = payload.get("monthly_quotas")
|
|
126
|
+
limited = payload.get("limited_user_quotas")
|
|
127
|
+
if not isinstance(monthly, dict) or not isinstance(limited, dict):
|
|
128
|
+
return []
|
|
129
|
+
windows: list[UsageWindow] = []
|
|
130
|
+
for name, key in (("premium_interactions", "completions"), ("chat", "chat")):
|
|
131
|
+
total = _number(monthly.get(key))
|
|
132
|
+
remaining = _number(limited.get(key))
|
|
133
|
+
if not total or remaining is None:
|
|
134
|
+
continue
|
|
135
|
+
percent_remaining = (remaining / total) * 100.0
|
|
136
|
+
windows.append(
|
|
137
|
+
UsageWindow(
|
|
138
|
+
name=name,
|
|
139
|
+
kind=UsageWindowKind.MONTHLY,
|
|
140
|
+
used_percent=_clamp_percent(100.0 - percent_remaining),
|
|
141
|
+
remaining_percent=_clamp_percent(percent_remaining),
|
|
142
|
+
remaining_units=remaining,
|
|
143
|
+
confidence=Confidence.OFFICIAL,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return windows
|
agentpool/usage/devin.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tomllib
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentpool.models import CapacitySnapshot, Confidence, UsageWindow, UsageWindowKind
|
|
11
|
+
from agentpool.usage._common import (
|
|
12
|
+
ProbeError,
|
|
13
|
+
_clamp_percent,
|
|
14
|
+
_epoch_seconds,
|
|
15
|
+
_number,
|
|
16
|
+
_tmux_slash_usage_probe,
|
|
17
|
+
_clean_optional_string,
|
|
18
|
+
_status_from_windows,
|
|
19
|
+
unavailable,
|
|
20
|
+
)
|
|
21
|
+
from agentpool.usage.provider_parsers import parse_devin_usage
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def devin_cli_usage_snapshot(provider_id: str, binary: str | None = None) -> CapacitySnapshot:
|
|
25
|
+
executable = binary or shutil.which("devin")
|
|
26
|
+
if not executable:
|
|
27
|
+
return unavailable(provider_id, "Devin CLI is not installed.")
|
|
28
|
+
try:
|
|
29
|
+
return _devin_plan_status_usage_snapshot(provider_id)
|
|
30
|
+
except ProbeError as exc:
|
|
31
|
+
fallback_warning = f"Devin plan-status API probe failed; fell back to CLI /usage: {exc}"
|
|
32
|
+
snapshot = _tmux_slash_usage_probe(
|
|
33
|
+
provider_id=provider_id,
|
|
34
|
+
command=[executable, "--permission-mode", "auto"],
|
|
35
|
+
slash_command="/usage",
|
|
36
|
+
parser=parse_devin_usage,
|
|
37
|
+
source="devin_pty_usage",
|
|
38
|
+
startup_delay=1.0,
|
|
39
|
+
timeout=18.0,
|
|
40
|
+
pre_keys=[["Enter"]],
|
|
41
|
+
prefer_text="Quota used:",
|
|
42
|
+
)
|
|
43
|
+
snapshot.warnings.append(fallback_warning)
|
|
44
|
+
return snapshot
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _devin_plan_status_usage_snapshot(provider_id: str) -> CapacitySnapshot:
|
|
48
|
+
creds = _load_devin_cli_credentials()
|
|
49
|
+
token = _clean_optional_string(creds.get("windsurf_api_key"))
|
|
50
|
+
if not token:
|
|
51
|
+
raise ProbeError("Devin CLI credentials do not contain windsurf_api_key.")
|
|
52
|
+
api_server_url = _clean_optional_string(creds.get("api_server_url")) or "https://server.codeium.com"
|
|
53
|
+
endpoint = api_server_url.rstrip("/") + "/exa.seat_management_pb.SeatManagementService/GetPlanStatus"
|
|
54
|
+
request = urllib.request.Request(
|
|
55
|
+
endpoint,
|
|
56
|
+
data=_encode_devin_plan_status_request(token),
|
|
57
|
+
headers={
|
|
58
|
+
"Content-Type": "application/proto",
|
|
59
|
+
"Connect-Protocol-Version": "1",
|
|
60
|
+
"Origin": "https://windsurf.com",
|
|
61
|
+
"Referer": "https://windsurf.com/profile",
|
|
62
|
+
"User-Agent": "agentpool-devin-probe/0.1",
|
|
63
|
+
"x-auth-token": token,
|
|
64
|
+
"x-devin-session-token": token,
|
|
65
|
+
},
|
|
66
|
+
method="POST",
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
70
|
+
data = response.read()
|
|
71
|
+
except urllib.error.HTTPError as exc:
|
|
72
|
+
body = exc.read().decode("utf-8", errors="replace")[:500].replace(token, "<redacted>")
|
|
73
|
+
raise ProbeError(f"HTTP {exc.code}: {body}") from exc
|
|
74
|
+
except urllib.error.URLError as exc:
|
|
75
|
+
raise ProbeError(str(exc.reason)) from exc
|
|
76
|
+
payload = decode_devin_plan_status_response(data)
|
|
77
|
+
snapshot = parse_devin_plan_status_response(provider_id, payload)
|
|
78
|
+
snapshot.raw["source"] = "devin_plan_status_api"
|
|
79
|
+
snapshot.raw["credential_source"] = "devin_cli_credentials"
|
|
80
|
+
return snapshot
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_devin_cli_credentials() -> dict[str, Any]:
|
|
84
|
+
path = Path("~/.local/share/devin/credentials.toml").expanduser()
|
|
85
|
+
if not path.exists():
|
|
86
|
+
raise ProbeError("Devin CLI credentials were not found. Run `devin auth login` first.")
|
|
87
|
+
try:
|
|
88
|
+
raw = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
89
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
90
|
+
raise ProbeError(f"Could not read Devin CLI credentials: {exc}") from exc
|
|
91
|
+
if not isinstance(raw, dict):
|
|
92
|
+
raise ProbeError("Devin CLI credentials did not parse as a TOML table.")
|
|
93
|
+
return raw
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_devin_plan_status_response(provider_id: str, payload: dict[str, Any]) -> CapacitySnapshot:
|
|
97
|
+
plan_status = payload.get("plan_status")
|
|
98
|
+
if not isinstance(plan_status, dict):
|
|
99
|
+
raise ProbeError("Devin plan-status response did not include plan_status.")
|
|
100
|
+
windows: list[UsageWindow] = []
|
|
101
|
+
for name, remaining_key, reset_key in (
|
|
102
|
+
("daily", "daily_remaining_percent", "daily_reset_at_unix"),
|
|
103
|
+
("weekly", "weekly_remaining_percent", "weekly_reset_at_unix"),
|
|
104
|
+
):
|
|
105
|
+
remaining = _number(plan_status.get(remaining_key))
|
|
106
|
+
if remaining is None:
|
|
107
|
+
continue
|
|
108
|
+
windows.append(
|
|
109
|
+
UsageWindow(
|
|
110
|
+
name=name,
|
|
111
|
+
kind=UsageWindowKind(name),
|
|
112
|
+
status=name,
|
|
113
|
+
used_percent=_clamp_percent(100.0 - remaining),
|
|
114
|
+
remaining_percent=_clamp_percent(remaining),
|
|
115
|
+
reset_at=_epoch_seconds(plan_status.get(reset_key)),
|
|
116
|
+
confidence=Confidence.OFFICIAL,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
overage_balance_micros = _number(plan_status.get("overage_balance_micros"))
|
|
120
|
+
credits_remaining = overage_balance_micros / 1_000_000 if overage_balance_micros is not None else None
|
|
121
|
+
if not windows and credits_remaining is None:
|
|
122
|
+
raise ProbeError("Devin plan-status response did not include quota windows or overage balance.")
|
|
123
|
+
plan_info = plan_status.get("plan_info") if isinstance(plan_status.get("plan_info"), dict) else {}
|
|
124
|
+
raw: dict[str, Any] = {
|
|
125
|
+
"plan_name": _clean_optional_string(plan_info.get("plan_name")),
|
|
126
|
+
"teams_tier": plan_info.get("teams_tier"),
|
|
127
|
+
"plan_start": _epoch_seconds(plan_status.get("plan_start_unix")).isoformat()
|
|
128
|
+
if _epoch_seconds(plan_status.get("plan_start_unix"))
|
|
129
|
+
else None,
|
|
130
|
+
"plan_end": _epoch_seconds(plan_status.get("plan_end_unix")).isoformat()
|
|
131
|
+
if _epoch_seconds(plan_status.get("plan_end_unix"))
|
|
132
|
+
else None,
|
|
133
|
+
"has_overage_balance": overage_balance_micros is not None,
|
|
134
|
+
}
|
|
135
|
+
return CapacitySnapshot(
|
|
136
|
+
provider_id=provider_id,
|
|
137
|
+
status=_status_from_windows(windows),
|
|
138
|
+
confidence=Confidence.OFFICIAL,
|
|
139
|
+
windows=windows,
|
|
140
|
+
credits_remaining=credits_remaining,
|
|
141
|
+
raw={key: value for key, value in raw.items() if value is not None},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _encode_devin_plan_status_request(auth_token: str) -> bytes:
|
|
146
|
+
data = bytearray()
|
|
147
|
+
_proto_append_key(data, 1, 2)
|
|
148
|
+
_proto_append_bytes(data, auth_token.encode("utf-8"))
|
|
149
|
+
_proto_append_key(data, 2, 0)
|
|
150
|
+
_proto_append_varint(data, 1)
|
|
151
|
+
return bytes(data)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def decode_devin_plan_status_response(data: bytes) -> dict[str, Any]:
|
|
155
|
+
plan_status: dict[str, Any] | None = None
|
|
156
|
+
for field_number, wire_type, value in _proto_fields(data):
|
|
157
|
+
if field_number == 1 and wire_type == 2 and isinstance(value, bytes):
|
|
158
|
+
plan_status = _decode_devin_plan_status(value)
|
|
159
|
+
return {"plan_status": plan_status} if plan_status is not None else {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _decode_devin_plan_status(data: bytes) -> dict[str, Any]:
|
|
163
|
+
plan_status: dict[str, Any] = {}
|
|
164
|
+
for field_number, wire_type, value in _proto_fields(data):
|
|
165
|
+
if field_number == 1 and wire_type == 2 and isinstance(value, bytes):
|
|
166
|
+
plan_status["plan_info"] = _decode_devin_plan_info(value)
|
|
167
|
+
elif field_number == 2 and wire_type == 2 and isinstance(value, bytes):
|
|
168
|
+
plan_status["plan_start_unix"] = _decode_timestamp_seconds(value)
|
|
169
|
+
elif field_number == 3 and wire_type == 2 and isinstance(value, bytes):
|
|
170
|
+
plan_status["plan_end_unix"] = _decode_timestamp_seconds(value)
|
|
171
|
+
elif field_number == 14 and wire_type == 0:
|
|
172
|
+
plan_status["daily_remaining_percent"] = value
|
|
173
|
+
elif field_number == 15 and wire_type == 0:
|
|
174
|
+
plan_status["weekly_remaining_percent"] = value
|
|
175
|
+
elif field_number == 16 and wire_type == 0:
|
|
176
|
+
plan_status["overage_balance_micros"] = value
|
|
177
|
+
elif field_number == 17 and wire_type == 0:
|
|
178
|
+
plan_status["daily_reset_at_unix"] = value
|
|
179
|
+
elif field_number == 18 and wire_type == 0:
|
|
180
|
+
plan_status["weekly_reset_at_unix"] = value
|
|
181
|
+
return plan_status
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _decode_devin_plan_info(data: bytes) -> dict[str, Any]:
|
|
185
|
+
plan_info: dict[str, Any] = {}
|
|
186
|
+
for field_number, wire_type, value in _proto_fields(data):
|
|
187
|
+
if field_number == 1 and wire_type == 0:
|
|
188
|
+
plan_info["teams_tier"] = value
|
|
189
|
+
elif field_number == 2 and wire_type == 2 and isinstance(value, bytes):
|
|
190
|
+
plan_info["plan_name"] = value.decode("utf-8", errors="replace")
|
|
191
|
+
return plan_info
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _decode_timestamp_seconds(data: bytes) -> int | None:
|
|
195
|
+
for field_number, wire_type, value in _proto_fields(data):
|
|
196
|
+
if field_number == 1 and wire_type == 0:
|
|
197
|
+
return int(value)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _proto_fields(data: bytes) -> list[tuple[int, int, int | bytes]]:
|
|
202
|
+
fields: list[tuple[int, int, int | bytes]] = []
|
|
203
|
+
index = 0
|
|
204
|
+
while index < len(data):
|
|
205
|
+
key, index = _proto_read_varint(data, index)
|
|
206
|
+
field_number = key >> 3
|
|
207
|
+
wire_type = key & 0x07
|
|
208
|
+
if wire_type == 0:
|
|
209
|
+
value, index = _proto_read_varint(data, index)
|
|
210
|
+
fields.append((field_number, wire_type, value))
|
|
211
|
+
elif wire_type == 1:
|
|
212
|
+
if index + 8 > len(data):
|
|
213
|
+
raise ProbeError("Truncated fixed64 protobuf field.")
|
|
214
|
+
fields.append((field_number, wire_type, data[index : index + 8]))
|
|
215
|
+
index += 8
|
|
216
|
+
elif wire_type == 2:
|
|
217
|
+
length, index = _proto_read_varint(data, index)
|
|
218
|
+
end = index + length
|
|
219
|
+
if end > len(data):
|
|
220
|
+
raise ProbeError("Truncated length-delimited protobuf field.")
|
|
221
|
+
fields.append((field_number, wire_type, data[index:end]))
|
|
222
|
+
index = end
|
|
223
|
+
elif wire_type == 5:
|
|
224
|
+
if index + 4 > len(data):
|
|
225
|
+
raise ProbeError("Truncated fixed32 protobuf field.")
|
|
226
|
+
fields.append((field_number, wire_type, data[index : index + 4]))
|
|
227
|
+
index += 4
|
|
228
|
+
else:
|
|
229
|
+
raise ProbeError(f"Unsupported protobuf wire type {wire_type}.")
|
|
230
|
+
return fields
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _proto_read_varint(data: bytes, index: int) -> tuple[int, int]:
|
|
234
|
+
shift = 0
|
|
235
|
+
value = 0
|
|
236
|
+
while index < len(data):
|
|
237
|
+
byte = data[index]
|
|
238
|
+
index += 1
|
|
239
|
+
value |= (byte & 0x7F) << shift
|
|
240
|
+
if not byte & 0x80:
|
|
241
|
+
return value, index
|
|
242
|
+
shift += 7
|
|
243
|
+
if shift > 70:
|
|
244
|
+
raise ProbeError("Malformed protobuf varint.")
|
|
245
|
+
raise ProbeError("Truncated protobuf varint.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _proto_append_key(data: bytearray, field_number: int, wire_type: int) -> None:
|
|
249
|
+
_proto_append_varint(data, (field_number << 3) | wire_type)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _proto_append_bytes(data: bytearray, value: bytes) -> None:
|
|
253
|
+
_proto_append_varint(data, len(value))
|
|
254
|
+
data.extend(value)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _proto_append_varint(data: bytearray, value: int) -> None:
|
|
258
|
+
while True:
|
|
259
|
+
byte = value & 0x7F
|
|
260
|
+
value >>= 7
|
|
261
|
+
if value:
|
|
262
|
+
data.append(byte | 0x80)
|
|
263
|
+
else:
|
|
264
|
+
data.append(byte)
|
|
265
|
+
return
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from agentpool.models import CapacitySnapshot, Confidence, UsageStatus, UsageWindow, UsageWindowKind
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_usage_warning(provider_id: str, text: str) -> CapacitySnapshot | None:
|
|
9
|
+
if re.search(r"limit reached", text, re.I):
|
|
10
|
+
return CapacitySnapshot(
|
|
11
|
+
provider_id=provider_id,
|
|
12
|
+
status=UsageStatus.LIMIT_REACHED,
|
|
13
|
+
confidence=Confidence.PROVIDER_WARNING,
|
|
14
|
+
windows=[
|
|
15
|
+
UsageWindow(
|
|
16
|
+
name="unknown",
|
|
17
|
+
kind=UsageWindowKind.UNKNOWN,
|
|
18
|
+
status="limit_reached",
|
|
19
|
+
confidence=Confidence.PROVIDER_WARNING,
|
|
20
|
+
raw_text=text,
|
|
21
|
+
)
|
|
22
|
+
],
|
|
23
|
+
warnings=[text.strip()],
|
|
24
|
+
)
|
|
25
|
+
if re.search(r"approaching .*limit", text, re.I):
|
|
26
|
+
return CapacitySnapshot(
|
|
27
|
+
provider_id=provider_id,
|
|
28
|
+
status=UsageStatus.NEAR_LIMIT,
|
|
29
|
+
confidence=Confidence.PROVIDER_WARNING,
|
|
30
|
+
windows=[
|
|
31
|
+
UsageWindow(
|
|
32
|
+
name="unknown",
|
|
33
|
+
kind=UsageWindowKind.UNKNOWN,
|
|
34
|
+
status="near_limit",
|
|
35
|
+
confidence=Confidence.PROVIDER_WARNING,
|
|
36
|
+
raw_text=text,
|
|
37
|
+
)
|
|
38
|
+
],
|
|
39
|
+
warnings=[text.strip()],
|
|
40
|
+
)
|
|
41
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentpool.usage._common import ProbeError, _extract_json_payload, unavailable, unknown
|
|
4
|
+
from agentpool.usage.ccusage import ccusage_usage_snapshot, detect_ccusage, parse_ccusage_blocks
|
|
5
|
+
from agentpool.usage.claude import claude_code_usage_snapshot
|
|
6
|
+
from agentpool.usage.codex import codex_cli_usage_snapshot, parse_codex_rate_limits
|
|
7
|
+
from agentpool.usage.codexbar import (
|
|
8
|
+
CODEXBAR_PROVIDER_MAP,
|
|
9
|
+
CODEXBAR_SAFE_SOURCE_MAP,
|
|
10
|
+
codexbar_usage_snapshot,
|
|
11
|
+
detect_codexbar,
|
|
12
|
+
parse_codexbar_usage,
|
|
13
|
+
)
|
|
14
|
+
from agentpool.usage.combine import combine_usage_snapshots
|
|
15
|
+
from agentpool.usage.copilot import copilot_cli_usage_snapshot, parse_copilot_usage_response
|
|
16
|
+
from agentpool.usage.devin import (
|
|
17
|
+
_encode_devin_plan_status_request,
|
|
18
|
+
_proto_append_bytes,
|
|
19
|
+
_proto_append_key,
|
|
20
|
+
_proto_append_varint,
|
|
21
|
+
decode_devin_plan_status_response,
|
|
22
|
+
devin_cli_usage_snapshot,
|
|
23
|
+
parse_devin_plan_status_response,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CODEXBAR_PROVIDER_MAP",
|
|
28
|
+
"CODEXBAR_SAFE_SOURCE_MAP",
|
|
29
|
+
"ProbeError",
|
|
30
|
+
"_encode_devin_plan_status_request",
|
|
31
|
+
"_extract_json_payload",
|
|
32
|
+
"_proto_append_bytes",
|
|
33
|
+
"_proto_append_key",
|
|
34
|
+
"_proto_append_varint",
|
|
35
|
+
"unavailable",
|
|
36
|
+
"unknown",
|
|
37
|
+
"ccusage_usage_snapshot",
|
|
38
|
+
"claude_code_usage_snapshot",
|
|
39
|
+
"codex_cli_usage_snapshot",
|
|
40
|
+
"codexbar_usage_snapshot",
|
|
41
|
+
"combine_usage_snapshots",
|
|
42
|
+
"copilot_cli_usage_snapshot",
|
|
43
|
+
"decode_devin_plan_status_response",
|
|
44
|
+
"detect_ccusage",
|
|
45
|
+
"detect_codexbar",
|
|
46
|
+
"devin_cli_usage_snapshot",
|
|
47
|
+
"parse_ccusage_blocks",
|
|
48
|
+
"parse_codex_rate_limits",
|
|
49
|
+
"parse_codexbar_usage",
|
|
50
|
+
"parse_copilot_usage_response",
|
|
51
|
+
"parse_devin_plan_status_response",
|
|
52
|
+
]
|