tsugite-claude-code 0.23.2__tar.gz → 0.24.0__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.
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/PKG-INFO +2 -2
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/pyproject.toml +2 -2
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/tsugite_claude_code/process.py +3 -1
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/tsugite_claude_code/provider.py +51 -45
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/tsugite_claude_code/usage.py +61 -22
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/.gitignore +0 -0
- {tsugite_claude_code-0.23.2 → tsugite_claude_code-0.24.0}/tsugite_claude_code/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tsugite-claude-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: Tsugite plugin: Claude Code provider (routes LLM calls through the claude CLI)
|
|
5
5
|
Author: Justyn Shull
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: tsugite-cli==0.
|
|
7
|
+
Requires-Dist: tsugite-cli==0.24.0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsugite-claude-code"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.24.0"
|
|
4
4
|
description = "Tsugite plugin: Claude Code provider (routes LLM calls through the claude CLI)"
|
|
5
5
|
authors = [{ name = "Justyn Shull" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"tsugite-cli==0.
|
|
8
|
+
"tsugite-cli==0.24.0",
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[project.entry-points."tsugite.providers"]
|
|
@@ -19,6 +19,7 @@ from collections.abc import AsyncIterator
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
21
|
from tsugite.cli.helpers import get_workspace_dir
|
|
22
|
+
from tsugite_claude_code.usage import record_rate_limit
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
@@ -368,7 +369,8 @@ class ClaudeCodeProcess:
|
|
|
368
369
|
}
|
|
369
370
|
return
|
|
370
371
|
|
|
371
|
-
|
|
372
|
+
elif event_type == "rate_limit_event":
|
|
373
|
+
record_rate_limit(event.get("rate_limit_info") or {})
|
|
372
374
|
|
|
373
375
|
async def stop(self) -> None:
|
|
374
376
|
"""Terminate subprocess and clean up temp files."""
|
|
@@ -14,62 +14,43 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
|
|
15
15
|
_CLAUDE_CODE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"]
|
|
16
16
|
|
|
17
|
+
# --- BEGIN GENERATED MODELS (claude_code) --- #
|
|
18
|
+
# Auto-generated by scripts/update_model_registry.py, do not edit manually
|
|
19
|
+
# fmt: off
|
|
17
20
|
_CLAUDE_CODE_MODELS: dict[str, ModelInfo] = {
|
|
18
|
-
"claude_code/claude-fable-5": ModelInfo(
|
|
19
|
-
|
|
20
|
-
),
|
|
21
|
-
"claude_code/claude-
|
|
22
|
-
|
|
23
|
-
),
|
|
24
|
-
"claude_code/claude-
|
|
25
|
-
|
|
26
|
-
),
|
|
27
|
-
"claude_code/claude-
|
|
28
|
-
|
|
29
|
-
),
|
|
30
|
-
"claude_code/claude-
|
|
31
|
-
|
|
32
|
-
),
|
|
33
|
-
"claude_code/claude-
|
|
34
|
-
|
|
35
|
-
),
|
|
36
|
-
"claude_code/claude-opus-4-6": ModelInfo(
|
|
37
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
38
|
-
),
|
|
39
|
-
"claude_code/claude-opus-4-7": ModelInfo(
|
|
40
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
41
|
-
),
|
|
42
|
-
"claude_code/claude-opus-4-8": ModelInfo(
|
|
43
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
44
|
-
),
|
|
45
|
-
"claude_code/claude-opus-5": ModelInfo(
|
|
46
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
47
|
-
),
|
|
48
|
-
"claude_code/claude-sonnet-4-5": ModelInfo(
|
|
49
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
50
|
-
),
|
|
51
|
-
"claude_code/claude-sonnet-4-5-20250929": ModelInfo(
|
|
52
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
53
|
-
),
|
|
54
|
-
"claude_code/claude-sonnet-4-6": ModelInfo(
|
|
55
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
56
|
-
),
|
|
57
|
-
"claude_code/claude-sonnet-5": ModelInfo(
|
|
58
|
-
max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS
|
|
59
|
-
),
|
|
21
|
+
"claude_code/claude-fable-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
22
|
+
"claude_code/claude-fable-5-1": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
23
|
+
"claude_code/claude-haiku-4-5": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
24
|
+
"claude_code/claude-haiku-4-5-20251001": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
25
|
+
"claude_code/claude-haiku-5-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
26
|
+
"claude_code/claude-opus-4-5": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
27
|
+
"claude_code/claude-opus-4-5-20251101": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
28
|
+
"claude_code/claude-opus-4-6": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
29
|
+
"claude_code/claude-opus-4-7": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
30
|
+
"claude_code/claude-opus-4-8": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
31
|
+
"claude_code/claude-opus-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
32
|
+
"claude_code/claude-opus-5-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
33
|
+
"claude_code/claude-sonnet-4-5": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
34
|
+
"claude_code/claude-sonnet-4-5-20250929": ModelInfo(max_input_tokens=200_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
35
|
+
"claude_code/claude-sonnet-4-6": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
36
|
+
"claude_code/claude-sonnet-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
37
|
+
"claude_code/claude-sonnet-5-5": ModelInfo(max_input_tokens=1_000_000, supports_vision=True, supported_effort_levels=_CLAUDE_CODE_EFFORT_LEVELS),
|
|
60
38
|
}
|
|
39
|
+
# fmt: on
|
|
40
|
+
# --- END GENERATED MODELS (claude_code) --- #
|
|
61
41
|
|
|
62
42
|
# Bare tier aliases track the newest model in that tier; version-pinned aliases
|
|
63
43
|
# stay for reproducible configs. Exception: opus stays on 4-8. Opus 5's replies
|
|
64
44
|
# through Claude Code's resume path can leave an empty assistant block that wedges
|
|
65
45
|
# the sidecar transcript (400 "text content blocks must be non-empty"), and the
|
|
66
|
-
# fresh-session fallback only covers the first send, so opus-5
|
|
67
|
-
# pinned
|
|
46
|
+
# fresh-session fallback only covers the first send, so opus-5 and opus-5-5 are
|
|
47
|
+
# opt-in via their pinned aliases until a mid-conversation poison recovers cleanly.
|
|
68
48
|
_ALIASES = {
|
|
69
49
|
"fable": "claude-fable-5-1",
|
|
70
50
|
"fable-5-1": "claude-fable-5-1",
|
|
71
51
|
"fable-5": "claude-fable-5",
|
|
72
52
|
"opus": "claude-opus-4-8",
|
|
53
|
+
"opus-5-5": "claude-opus-5-5",
|
|
73
54
|
"opus-5": "claude-opus-5",
|
|
74
55
|
"opus-4-8": "claude-opus-4-8",
|
|
75
56
|
"opus-4-7": "claude-opus-4-7",
|
|
@@ -77,11 +58,13 @@ _ALIASES = {
|
|
|
77
58
|
"opus-4-5": "claude-opus-4-5",
|
|
78
59
|
"opus-4-5-20251101": "claude-opus-4-5-20251101",
|
|
79
60
|
"sonnet": "claude-sonnet-5",
|
|
61
|
+
"sonnet-5-5": "claude-sonnet-5-5",
|
|
80
62
|
"sonnet-5": "claude-sonnet-5",
|
|
81
63
|
"sonnet-4-6": "claude-sonnet-4-6",
|
|
82
64
|
"sonnet-4-5": "claude-sonnet-4-5",
|
|
83
65
|
"sonnet-4-5-20250929": "claude-sonnet-4-5-20250929",
|
|
84
|
-
"haiku": "claude-haiku-
|
|
66
|
+
"haiku": "claude-haiku-5-5",
|
|
67
|
+
"haiku-5-5": "claude-haiku-5-5",
|
|
85
68
|
"haiku-4-5": "claude-haiku-4-5",
|
|
86
69
|
"haiku-4-5-20251001": "claude-haiku-4-5-20251001",
|
|
87
70
|
}
|
|
@@ -150,6 +133,8 @@ class ClaudeCodeProvider:
|
|
|
150
133
|
self._resume_session = kwargs.get("resume_session")
|
|
151
134
|
self._resume_after_compaction = kwargs.get("resume_after_compaction", False)
|
|
152
135
|
self._previous_messages = kwargs.get("previous_messages", [])
|
|
136
|
+
# total_cost_usd is cumulative for the whole --resume'd session, not this process.
|
|
137
|
+
self._cumulative_cost = kwargs.get("resume_cost_baseline") or 0.0
|
|
153
138
|
|
|
154
139
|
def get_state(self) -> dict | None:
|
|
155
140
|
return {
|
|
@@ -158,6 +143,7 @@ class ClaudeCodeProvider:
|
|
|
158
143
|
"context_window": self._context_window,
|
|
159
144
|
"cache_creation_tokens": self._cache_creation_tokens,
|
|
160
145
|
"cache_read_tokens": self._cache_read_tokens,
|
|
146
|
+
"cumulative_cost": self._cumulative_cost,
|
|
161
147
|
}
|
|
162
148
|
|
|
163
149
|
async def acompletion(
|
|
@@ -236,6 +222,7 @@ class ClaudeCodeProvider:
|
|
|
236
222
|
await self._process.stop()
|
|
237
223
|
self._resume_session = None
|
|
238
224
|
self._resume_after_compaction = False
|
|
225
|
+
self._cumulative_cost = 0.0
|
|
239
226
|
self._process = ClaudeCodeProcess()
|
|
240
227
|
await self._process.start(
|
|
241
228
|
model=self._resolved_model,
|
|
@@ -350,6 +337,9 @@ class ClaudeCodeProvider:
|
|
|
350
337
|
header += ">"
|
|
351
338
|
parts.append(header + "\n" + "\n\n".join(history_lines) + "\n</conversation_history>\n")
|
|
352
339
|
|
|
340
|
+
if self._resume_session:
|
|
341
|
+
parts.extend(self._unseen_delivery_cards())
|
|
342
|
+
|
|
353
343
|
# Add the task (last user message — earlier user messages are context/history)
|
|
354
344
|
for msg in reversed(messages):
|
|
355
345
|
if msg.get("role") == "user":
|
|
@@ -379,6 +369,22 @@ class ClaudeCodeProvider:
|
|
|
379
369
|
blocks.insert(0, {"type": "text", "text": text})
|
|
380
370
|
return blocks
|
|
381
371
|
|
|
372
|
+
def _unseen_delivery_cards(self) -> list[str]:
|
|
373
|
+
"""FYI delivery cards recorded after the last assistant reply.
|
|
374
|
+
|
|
375
|
+
A resumed session skips stored history and the CLI transcript never saw
|
|
376
|
+
these cards. needs_ack cards already render in <pending_deliveries>.
|
|
377
|
+
"""
|
|
378
|
+
last_reply = max((i for i, m in enumerate(self._previous_messages) if m.get("role") == "assistant"), default=-1)
|
|
379
|
+
cards = []
|
|
380
|
+
for msg in self._previous_messages[last_reply + 1 :]:
|
|
381
|
+
content = msg.get("content")
|
|
382
|
+
if not isinstance(content, str) or not content.startswith("<tsugite_delivery "):
|
|
383
|
+
continue
|
|
384
|
+
if 'kind="needs_ack"' not in content.split("\n", 1)[0]:
|
|
385
|
+
cards.append(content + "\n")
|
|
386
|
+
return cards
|
|
387
|
+
|
|
382
388
|
def _image_blocks(self) -> list[dict]:
|
|
383
389
|
"""base64 image `source` payloads for the API-supported image attachments.
|
|
384
390
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Claude Code subscription limits as a tsugite quota source.
|
|
2
2
|
|
|
3
3
|
Read from ``limits[]`` in the usage endpoint Claude Code calls, with its OAuth
|
|
4
|
-
token.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
token. The endpoint rate-limits hard, so it is called at most hourly and backs
|
|
5
|
+
off after a failure, and each turn's ``rate_limit_event`` keeps the session and
|
|
6
|
+
weekly windows current in between. With no endpoint reading, the usage copy
|
|
7
|
+
cached in Claude Code's config is used while it is under an hour old. The list
|
|
8
|
+
is self-describing, so a limit kind tsugite has never seen still shows up.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
@@ -12,6 +14,7 @@ import json
|
|
|
12
14
|
import logging
|
|
13
15
|
import os
|
|
14
16
|
import time
|
|
17
|
+
from datetime import datetime, timezone
|
|
15
18
|
from pathlib import Path
|
|
16
19
|
|
|
17
20
|
import httpx
|
|
@@ -25,9 +28,14 @@ PROVIDER = "claude_code"
|
|
|
25
28
|
LABEL = "Claude Code"
|
|
26
29
|
USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
|
|
27
30
|
CACHE_MAX_AGE_MS = 60 * 60 * 1000
|
|
28
|
-
LIVE_TTL_S = 60
|
|
31
|
+
LIVE_TTL_S = 60 * 60
|
|
32
|
+
BACKOFF_S = (5 * 60, 10 * 60, 20 * 60, 60 * 60)
|
|
33
|
+
TURN_WINDOWS = {"five_hour": "session", "seven_day": "weekly_all"}
|
|
29
34
|
|
|
30
35
|
_last_live: tuple[float, list] | None = None
|
|
36
|
+
_failures = 0
|
|
37
|
+
_retry_at = 0.0
|
|
38
|
+
_turn: dict[str, tuple[float, QuotaWindow]] = {}
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
def _config_path() -> Path:
|
|
@@ -40,25 +48,35 @@ def _credentials_path() -> Path:
|
|
|
40
48
|
return Path(base) / ".credentials.json"
|
|
41
49
|
|
|
42
50
|
|
|
43
|
-
def
|
|
44
|
-
global _last_live
|
|
45
|
-
if _last_live and time.monotonic() - _last_live[0] < LIVE_TTL_S:
|
|
46
|
-
return _last_live[1]
|
|
51
|
+
def _request_limits() -> list:
|
|
47
52
|
token = json.loads(_credentials_path().read_text())["claudeAiOauth"]["accessToken"]
|
|
48
53
|
headers = {"Authorization": f"Bearer {token}", "anthropic-beta": "oauth-2025-04-20"}
|
|
49
54
|
set_user_agent_header(headers)
|
|
50
55
|
response = httpx.get(USAGE_URL, headers=headers, timeout=10)
|
|
51
56
|
response.raise_for_status()
|
|
52
|
-
|
|
53
|
-
_last_live = (time.monotonic(), limits)
|
|
54
|
-
return limits
|
|
57
|
+
return response.json()["limits"]
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def
|
|
60
|
+
def _refresh_live() -> None:
|
|
61
|
+
global _last_live, _failures, _retry_at
|
|
62
|
+
now = time.time()
|
|
63
|
+
if now < _retry_at:
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
_last_live = (now, _request_limits())
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning("Claude Code usage endpoint failed: %r", e)
|
|
69
|
+
_retry_at = now + BACKOFF_S[min(_failures, len(BACKOFF_S) - 1)]
|
|
70
|
+
_failures += 1
|
|
71
|
+
return
|
|
72
|
+
_failures, _retry_at = 0, now + LIVE_TTL_S
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cached_limits() -> tuple[float, list]:
|
|
58
76
|
cache = json.loads(_config_path().read_text())["cachedUsageUtilization"]
|
|
59
77
|
if time.time() * 1000 - cache["fetchedAtMs"] > CACHE_MAX_AGE_MS:
|
|
60
78
|
raise ValueError("cached usage is over an hour old")
|
|
61
|
-
return cache["utilization"]["limits"]
|
|
79
|
+
return cache["fetchedAtMs"] / 1000, cache["utilization"]["limits"]
|
|
62
80
|
|
|
63
81
|
|
|
64
82
|
def _label(limit: dict) -> str:
|
|
@@ -72,22 +90,41 @@ def _label(limit: dict) -> str:
|
|
|
72
90
|
return kind
|
|
73
91
|
|
|
74
92
|
|
|
93
|
+
def record_rate_limit(info: dict) -> None:
|
|
94
|
+
"""Keep the windows from a turn's ``rate_limit_event`` (``rate_limit_info``)."""
|
|
95
|
+
windows = info.get("unifiedWindows") or {}
|
|
96
|
+
for name, key in TURN_WINDOWS.items():
|
|
97
|
+
window = windows.get(name)
|
|
98
|
+
if not window or window.get("utilization") is None:
|
|
99
|
+
continue
|
|
100
|
+
resets_at = window.get("resetsAt")
|
|
101
|
+
_turn[key] = (
|
|
102
|
+
time.time(),
|
|
103
|
+
QuotaWindow(
|
|
104
|
+
key=key,
|
|
105
|
+
label=_label({"kind": key}),
|
|
106
|
+
used_pct=round(window["utilization"] * 100, 1),
|
|
107
|
+
resets_at=datetime.fromtimestamp(resets_at, timezone.utc).isoformat() if resets_at else None,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
75
112
|
def _error(message: str) -> QuotaReport:
|
|
76
113
|
return QuotaReport(provider=PROVIDER, label=LABEL, error=message)
|
|
77
114
|
|
|
78
115
|
|
|
79
116
|
def fetch() -> QuotaReport:
|
|
80
117
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
logger.warning("Claude Code usage unavailable: live %r, cache %r", live_error, cache_error)
|
|
118
|
+
_refresh_live()
|
|
119
|
+
fetched_at, limits = _last_live or _cached_limits()
|
|
120
|
+
except Exception as e:
|
|
121
|
+
if not _turn:
|
|
122
|
+
logger.warning("Claude Code usage unavailable: %r", e)
|
|
87
123
|
return _error("Claude Code's usage limits could not be fetched, and no recent cached copy exists")
|
|
124
|
+
fetched_at, limits = 0.0, []
|
|
88
125
|
|
|
89
126
|
try:
|
|
90
|
-
windows =
|
|
127
|
+
windows = [
|
|
91
128
|
QuotaWindow(
|
|
92
129
|
key=limit["kind"],
|
|
93
130
|
label=_label(limit),
|
|
@@ -96,11 +133,13 @@ def fetch() -> QuotaReport:
|
|
|
96
133
|
)
|
|
97
134
|
for limit in limits
|
|
98
135
|
if limit.get("percent") is not None
|
|
99
|
-
|
|
136
|
+
]
|
|
100
137
|
except Exception:
|
|
101
138
|
return _error("Claude Code's usage limits are not in a shape tsugite understands")
|
|
102
139
|
|
|
103
|
-
|
|
140
|
+
turn = {key: window for key, (read_at, window) in _turn.items() if read_at > fetched_at}
|
|
141
|
+
windows = [*turn.values(), *(w for w in windows if w.key not in turn)]
|
|
142
|
+
return QuotaReport(provider=PROVIDER, label=LABEL, windows=tuple(windows))
|
|
104
143
|
|
|
105
144
|
|
|
106
145
|
register_quota_source(PROVIDER, LABEL, fetch)
|
|
File without changes
|
|
File without changes
|