claude-multiacc 2.0.50 → 2.0.52
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/bin/codex +55 -11
- package/bin/codex-accounts +34 -40
- package/bin/multiacc-select +6 -1
- package/docs/CODEX.md +118 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/autoresume.py +7 -2
- package/lib/autoresume_probe.py +3 -3
- package/lib/codex_credit_controls.py +27 -0
- package/lib/codex_credit_local_scope.py +35 -0
- package/lib/codex_credit_selection.py +172 -0
- package/lib/codex_credits.py +158 -0
- package/lib/codex_limits_lock.py +56 -0
- package/lib/codex_usage_fetch.py +85 -0
- package/lib/report.py +6 -0
- package/lib/selector_credit_scope.py +85 -0
- package/lib/selector_credits.py +105 -0
- package/lib/selector_funding.py +113 -0
- package/lib/selector_policy.py +20 -2
- package/lib/selector_primitives.py +20 -2
- package/package.json +1 -1
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/test_codex_reset_credits.py +154 -0
- package/tests/test_codex_reset_credits_polling.py +151 -0
- package/tests/test_codex_reset_credits_report.py +85 -0
- package/tests/test_codex_reset_fetch.py +123 -0
- package/tests/test_selector_cached_credits.py +135 -0
- package/tests/test_selector_credit_scope.py +118 -0
- package/tests/test_selector_credit_shim.py +139 -0
- package/tests/test_selector_credits.py +140 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Pure, fail-closed credit telemetry normalization; no files or provider calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from decimal import Decimal, InvalidOperation
|
|
7
|
+
|
|
8
|
+
from selector_primitives import DECIMAL_RE, timestamp
|
|
9
|
+
|
|
10
|
+
CAPABILITY = "codex_credits_v1"
|
|
11
|
+
HARD_CREDIT_ERRORS = {"account_mismatch", "unauthenticated", "refusal_read_failed"}
|
|
12
|
+
CREDIT_FIELDS = {
|
|
13
|
+
"has_credits", "unlimited", "balance", "fetched_at", "account_id", "ordinary_usage_allowed",
|
|
14
|
+
"spend_control_reached", "limit_reached_type", "last_error", "last_error_at", "stale", "age_seconds"}
|
|
15
|
+
OPTIONAL_CANDIDATE_FIELDS = {
|
|
16
|
+
"credits", "credit_capable", "credit_active_holders", "credit_blocked_at", "credit_blocked_balance",
|
|
17
|
+
"limited_reason", "credit_actor_email"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def balance_value(value: object) -> str | None:
|
|
21
|
+
"""Credits are arbitrary nonnegative decimals, never percentages or money."""
|
|
22
|
+
if not isinstance(value, str) or len(value) > 100 or not DECIMAL_RE.fullmatch(value):
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
number = Decimal(value)
|
|
26
|
+
if number < 0 or not number.is_finite():
|
|
27
|
+
return None
|
|
28
|
+
plain = format(number, "f")
|
|
29
|
+
return plain.rstrip("0").rstrip(".") if "." in plain else plain
|
|
30
|
+
except (InvalidOperation, ValueError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def credit_candidate_error(raw: dict, field: str) -> str | None:
|
|
35
|
+
if "credit_capable" in raw and not isinstance(raw["credit_capable"], bool):
|
|
36
|
+
return f"{field}.credit_capable"
|
|
37
|
+
holders = raw.get("credit_active_holders", 0)
|
|
38
|
+
if isinstance(holders, bool) or not isinstance(holders, int) or not 0 <= holders <= 1_000_000:
|
|
39
|
+
return f"{field}.credit_active_holders"
|
|
40
|
+
for name in ("credit_blocked_at", "credit_blocked_balance", "limited_reason", "credit_actor_email"):
|
|
41
|
+
if raw.get(name) is not None and not isinstance(raw[name], str):
|
|
42
|
+
return f"{field}.{name}"
|
|
43
|
+
actor = raw.get("credit_actor_email")
|
|
44
|
+
if actor is not None and (len(actor) > 320 or any(c in actor for c in "\r\n\0")):
|
|
45
|
+
return f"{field}.credit_actor_email"
|
|
46
|
+
credits = raw.get("credits")
|
|
47
|
+
if credits is None:
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(credits, dict) or set(credits) - CREDIT_FIELDS:
|
|
50
|
+
return f"{field}.credits"
|
|
51
|
+
for name in ("balance", "fetched_at", "account_id", "limit_reached_type", "last_error", "last_error_at"):
|
|
52
|
+
if credits.get(name) is not None and not isinstance(credits[name], str):
|
|
53
|
+
return f"{field}.credits.{name}"
|
|
54
|
+
for name in ("has_credits", "unlimited", "ordinary_usage_allowed", "spend_control_reached", "stale"):
|
|
55
|
+
if credits.get(name) is not None and not isinstance(credits[name], bool):
|
|
56
|
+
return f"{field}.credits.{name}"
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_credits(raw: object, now: datetime, max_age: int = 900) -> dict:
|
|
61
|
+
raw = raw if isinstance(raw, dict) else {}
|
|
62
|
+
balance = balance_value(raw.get("balance"))
|
|
63
|
+
account = raw.get("account_id")
|
|
64
|
+
account = account.strip() if isinstance(account, str) and len(account) <= 256 else None
|
|
65
|
+
try:
|
|
66
|
+
observed, fetched_at = timestamp(raw.get("fetched_at"))
|
|
67
|
+
age = (now - observed).total_seconds()
|
|
68
|
+
fresh = -30 <= age <= max_age and raw.get("stale") is not True
|
|
69
|
+
if age < -30:
|
|
70
|
+
fetched_at = None
|
|
71
|
+
except (ValueError, TypeError):
|
|
72
|
+
fetched_at, fresh = None, False
|
|
73
|
+
reached = raw.get("limit_reached_type")
|
|
74
|
+
reached = reached.lower() if isinstance(reached, str) else None
|
|
75
|
+
restricted = raw.get("spend_control_reached") is True or reached not in {
|
|
76
|
+
None, "", "rate_limit_reached", "usage_limit_reached", "weekly_limit_reached", "session_limit_reached"}
|
|
77
|
+
available = (raw.get("unlimited") is True or
|
|
78
|
+
(raw.get("has_credits") is True and balance is not None and Decimal(balance) > 0))
|
|
79
|
+
return {
|
|
80
|
+
"has_credits": raw.get("has_credits") is True, "unlimited": raw.get("unlimited") is True,
|
|
81
|
+
"balance": balance, "account_id": account or None, "fetched_at": fetched_at,
|
|
82
|
+
"ordinary_usage_allowed": raw.get("ordinary_usage_allowed")
|
|
83
|
+
if isinstance(raw.get("ordinary_usage_allowed"), bool) else None,
|
|
84
|
+
"spend_control_reached": restricted, "limit_reached_type": reached,
|
|
85
|
+
"last_error": raw.get("last_error") or False, "fresh": fresh,
|
|
86
|
+
"available": bool(fetched_at and account and available and not restricted
|
|
87
|
+
and raw.get("last_error") not in HARD_CREDIT_ERRORS)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def refusal_recovered(raw: dict, credits: dict, now: datetime) -> bool:
|
|
91
|
+
"""Retry a funded wallet only after cooldown and a newer successful observation."""
|
|
92
|
+
blocked = raw.get("credit_blocked_at")
|
|
93
|
+
if not blocked:
|
|
94
|
+
return raw.get("limited_reason") != "credits"
|
|
95
|
+
try:
|
|
96
|
+
blocked_time, _ = timestamp(blocked)
|
|
97
|
+
fetched, _ = timestamp(credits.get("fetched_at"))
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
return False
|
|
100
|
+
old = balance_value(raw.get("credit_blocked_balance"))
|
|
101
|
+
current = credits.get("balance")
|
|
102
|
+
increased = old is not None and current is not None and Decimal(current) > Decimal(old)
|
|
103
|
+
cooled = (now - blocked_time).total_seconds() >= 600
|
|
104
|
+
return bool(fetched > blocked_time and credits["available"] and credits["fresh"]
|
|
105
|
+
and not credits["last_error"] and (increased or cooled))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Opted-in funding tiers preserve the legacy session gate and weekly band."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal, localcontext
|
|
6
|
+
|
|
7
|
+
from selector_credits import CAPABILITY, normalize_credits, refusal_recovered
|
|
8
|
+
from selector_credit_scope import (
|
|
9
|
+
actor_email, actor_key, actor_observations, actor_refusals, merge_actor, merge_wallet, wallet_available,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def wallet_snapshots(candidates: list[dict], now, max_age: int) -> dict:
|
|
14
|
+
wallets = {}
|
|
15
|
+
for raw in candidates:
|
|
16
|
+
credits = normalize_credits(raw.get("credits"), now, max_age)
|
|
17
|
+
key = credits["account_id"]
|
|
18
|
+
if not key:
|
|
19
|
+
continue
|
|
20
|
+
# The snapshot predates a failed refresh: its confirmed money remains true
|
|
21
|
+
# even when this slot's current credentials no longer authenticate.
|
|
22
|
+
credits["available"] = wallet_available(dict(credits, last_error=False))
|
|
23
|
+
observed = credits["fetched_at"] or ""
|
|
24
|
+
confirmed = bool(observed) and (credits["unlimited"]
|
|
25
|
+
or (raw.get("credits") or {}).get("has_credits") is False
|
|
26
|
+
or (credits["has_credits"] and credits["balance"] is not None))
|
|
27
|
+
credits["wallet_observed_at"], credits["wallet_confirmed"] = observed, confirmed
|
|
28
|
+
previous = wallets.get(key)
|
|
29
|
+
rank = (confirmed, observed, not credits["available"])
|
|
30
|
+
if previous is None or rank > (previous["wallet_confirmed"], previous["wallet_observed_at"],
|
|
31
|
+
not previous["available"]):
|
|
32
|
+
wallets[key] = credits
|
|
33
|
+
return wallets
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def apply_funding(rows: list[dict], candidates: list[dict], now, request: dict) -> None:
|
|
37
|
+
max_age = request.get("credit_max_age_seconds", 900)
|
|
38
|
+
wallets = wallet_snapshots(candidates, now, max_age)
|
|
39
|
+
refusals = actor_refusals(candidates)
|
|
40
|
+
observations = actor_observations(candidates, now, max_age)
|
|
41
|
+
holders = {}
|
|
42
|
+
for raw in candidates:
|
|
43
|
+
account = (raw.get("credits") or {}).get("account_id")
|
|
44
|
+
holders[account] = max(holders.get(account, 0), raw.get("credit_active_holders", 0))
|
|
45
|
+
for row, raw in zip(rows, candidates):
|
|
46
|
+
credits = normalize_credits(raw.get("credits"), now, max_age)
|
|
47
|
+
credits = merge_actor(credits, observations.get(actor_key(raw)))
|
|
48
|
+
credits = merge_wallet(credits, wallets.get(credits["account_id"]))
|
|
49
|
+
receipt = dict(raw, **refusals.get(actor_key(raw), {}))
|
|
50
|
+
known = row["quota_known"]
|
|
51
|
+
measured = [Decimal(row[key]) for key in ("weekly_pct", "session_pct") if row[key] is not None]
|
|
52
|
+
peak = max(measured) if measured else None
|
|
53
|
+
exhausted = peak is not None and peak >= 100
|
|
54
|
+
exhausted |= row["engine"] == "codex" and credits["ordinary_usage_allowed"] is False
|
|
55
|
+
reserve = peak is not None and 90 <= peak < 100
|
|
56
|
+
tier = "credits" if exhausted else "reserve" if reserve else "included"
|
|
57
|
+
recovered = refusal_recovered(receipt, credits, now)
|
|
58
|
+
reason = raw.get("limited_reason")
|
|
59
|
+
usage_park = reason == "usage" or (reason == "credits" and recovered)
|
|
60
|
+
park = row["exclusion_code"] in {"status_not_active", "account_limited"}
|
|
61
|
+
safe = row["status"] in {"active", "limited"} and (not park or usage_park)
|
|
62
|
+
paid = (row["engine"] == "codex" and raw.get("credit_capable") is True
|
|
63
|
+
and credits["available"] and recovered and safe)
|
|
64
|
+
# Retry normalization with just usage parks removed; independent auth, stale
|
|
65
|
+
# heartbeat, incapable provider and active reservations retain their exclusions.
|
|
66
|
+
if park and safe and ((tier == "credits" and paid) or (tier == "reserve" and known)):
|
|
67
|
+
from selector_policy import normalize_candidate
|
|
68
|
+
unparked = dict(raw, status="active", limited_until=None)
|
|
69
|
+
refreshed = normalize_candidate(unparked, now, row["input_ordinal"])
|
|
70
|
+
row["eligible"], row["exclusion_code"] = refreshed["eligible"], refreshed["exclusion_code"]
|
|
71
|
+
if row["eligible"] and tier == "credits" and not paid:
|
|
72
|
+
row["eligible"], row["exclusion_code"] = False, "credits_unavailable"
|
|
73
|
+
required = request.get("required_funding_tier")
|
|
74
|
+
if row["eligible"] and required and required != tier:
|
|
75
|
+
row["eligible"], row["exclusion_code"] = False, "funding_tier_excluded"
|
|
76
|
+
row.update(funding_tier=tier, credits=credits, credit_capable=raw.get("credit_capable") is True,
|
|
77
|
+
credit_active_holders=holders.get(credits["account_id"], 0),
|
|
78
|
+
credit_blocked_at=receipt.get("credit_blocked_at"),
|
|
79
|
+
credit_blocked_balance=receipt.get("credit_blocked_balance"), limited_reason=reason)
|
|
80
|
+
if "credit_actor_email" in raw:
|
|
81
|
+
row["credit_actor_email"] = actor_email(raw)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def best_funding_tier(rows: list[dict]) -> list[dict]:
|
|
85
|
+
order = {"included": 0, "reserve": 1, "credits": 2}
|
|
86
|
+
best = min(order[row["funding_tier"]] for row in rows)
|
|
87
|
+
return [row for row in rows if order[row["funding_tier"]] == best]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def credit_rank(row: dict) -> tuple:
|
|
91
|
+
credits, holders = row["credits"], row["credit_active_holders"]
|
|
92
|
+
with localcontext() as context:
|
|
93
|
+
context.prec = 110
|
|
94
|
+
share = Decimal(credits["balance"] or "0") / (holders + 1)
|
|
95
|
+
return (not credits["unlimited"], holders if credits["unlimited"] else -share)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def funding_proof(request: dict, winner: dict) -> dict:
|
|
99
|
+
return {"credit_capability": CAPABILITY, "credit_fallback": True,
|
|
100
|
+
"required_funding_tier": request.get("required_funding_tier"),
|
|
101
|
+
"credit_max_age_seconds": request.get("credit_max_age_seconds", 900),
|
|
102
|
+
"funding_tier": winner["funding_tier"]}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def funding_score(winner: dict) -> dict:
|
|
106
|
+
score = {"funding_tier": winner["funding_tier"],
|
|
107
|
+
"credit_balance": winner["credits"]["balance"],
|
|
108
|
+
"credit_account_id": winner["credits"]["account_id"],
|
|
109
|
+
"credit_active_holders": winner["credit_active_holders"],
|
|
110
|
+
"credit_unlimited": winner["credits"]["unlimited"]}
|
|
111
|
+
if "credit_actor_email" in winner:
|
|
112
|
+
score["credit_actor_email"] = winner["credit_actor_email"]
|
|
113
|
+
return score
|
package/lib/selector_policy.py
CHANGED
|
@@ -5,6 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from decimal import Decimal, localcontext
|
|
7
7
|
|
|
8
|
+
from selector_funding import (
|
|
9
|
+
apply_funding, best_funding_tier, credit_rank, funding_proof, funding_score,
|
|
10
|
+
)
|
|
8
11
|
from selector_primitives import (
|
|
9
12
|
DEFAULT_HEADROOM_BAND,
|
|
10
13
|
DEFAULT_SESSION_GATE,
|
|
@@ -207,25 +210,35 @@ def select(request: object) -> dict:
|
|
|
207
210
|
duplicate = next((items for items in identities.values() if len(items) > 1), None)
|
|
208
211
|
if duplicate:
|
|
209
212
|
return _error("duplicate_candidate_identity", {"input_ordinals": duplicate})
|
|
213
|
+
credit_mode = request.get("credit_fallback") is True
|
|
214
|
+
if credit_mode:
|
|
215
|
+
apply_funding(rows, request["candidates"], now, request)
|
|
210
216
|
producer, alternatives = _apply_policy(request, rows)
|
|
211
217
|
eligible = [row for row in rows if row["eligible"]]
|
|
212
218
|
if not eligible:
|
|
213
219
|
return _error("no_candidate", {"eligible_count": 0})
|
|
220
|
+
funded = best_funding_tier(eligible) if credit_mode else eligible
|
|
214
221
|
band, band_text = decimal_value(request.get("headroom_band", DEFAULT_HEADROOM_BAND))
|
|
215
222
|
gate, gate_text = decimal_value(request.get("session_gate", DEFAULT_SESSION_GATE))
|
|
216
223
|
# Two cuts, in this order (operator's 2026-09-03 decision): the session gate says
|
|
217
224
|
# WHO may be considered, the weekly band says which of those count as equally
|
|
218
225
|
# good. "healthy or known" is the gate stepping aside when nobody clears it.
|
|
219
|
-
known = [row for row in
|
|
226
|
+
known = [row for row in funded if row["quota_known"]]
|
|
220
227
|
healthy = _session_gate(known, gate)
|
|
221
228
|
ranked = healthy or known
|
|
222
229
|
floor = _band_floor(ranked, band) if band is not None else None
|
|
223
230
|
if floor is None:
|
|
224
231
|
# No usable quota anywhere: every eligible row is an unknown peer.
|
|
225
232
|
# Keep LRU and the request spread instead of preferring a fixed identity.
|
|
226
|
-
banded =
|
|
233
|
+
banded = funded
|
|
227
234
|
else:
|
|
228
235
|
banded = [row for row in ranked if Decimal(row["weekly_remaining"]) >= floor]
|
|
236
|
+
if credit_mode and funded[0]["funding_tier"] == "credits":
|
|
237
|
+
# Subscription windows cannot rank credit-funded work. Credit capacity per
|
|
238
|
+
# identity holder decides, with the same deterministic LRU/spread for ties.
|
|
239
|
+
best_credit = min(credit_rank(row) for row in funded)
|
|
240
|
+
banded = [row for row in funded if credit_rank(row) == best_credit]
|
|
241
|
+
floor, healthy = None, []
|
|
229
242
|
winner = min(banded, key=_band_key(request["reservation_key"]))
|
|
230
243
|
rows.sort(key=_snapshot_key)
|
|
231
244
|
snapshot = canonical_sha256(rows)
|
|
@@ -239,6 +252,8 @@ def select(request: object) -> dict:
|
|
|
239
252
|
"headroom_band": band_text,
|
|
240
253
|
"session_gate": gate_text,
|
|
241
254
|
"candidate_snapshot_digest": snapshot, "selected_identity": chosen}
|
|
255
|
+
if credit_mode:
|
|
256
|
+
digest_input.update(funding_proof(request, winner))
|
|
242
257
|
score_fields = ("quota_known", "weekly_pct", "session_pct", "weekly_remaining",
|
|
243
258
|
"session_remaining", "effective_headroom", "last_selected_at")
|
|
244
259
|
response = {"schema": SCHEMA, "selector_version": SELECTOR_VERSION, "ok": True, **chosen,
|
|
@@ -256,6 +271,9 @@ def select(request: object) -> dict:
|
|
|
256
271
|
"session_ok_count": len(healthy),
|
|
257
272
|
"candidate_snapshot_digest": snapshot,
|
|
258
273
|
"selection_digest": canonical_sha256(digest_input)}
|
|
274
|
+
if credit_mode:
|
|
275
|
+
response.update(funding_proof(request, winner))
|
|
276
|
+
response["score_basis"].update(funding_score(winner))
|
|
259
277
|
if request["policy"] == "reviewer" and not alternatives and _row_identity(winner) == producer:
|
|
260
278
|
response["fallback_reason"] = "sole_eligible_account"
|
|
261
279
|
return response
|
|
@@ -39,7 +39,8 @@ REQUEST_KEYS = {
|
|
|
39
39
|
# headroom_band from its own setting and never sends session_gate, so the gate default
|
|
40
40
|
# below is what every panel launch gets (operator's 2026-09-03 decision: session
|
|
41
41
|
# headroom gates FIRST, then the weekly band picks among the survivors).
|
|
42
|
-
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate"
|
|
42
|
+
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate", "credit_fallback",
|
|
43
|
+
"required_funding_tier", "credit_max_age_seconds"}
|
|
43
44
|
DEFAULT_HEADROOM_BAND = "30"
|
|
44
45
|
DEFAULT_SESSION_GATE = "50"
|
|
45
46
|
POLICIES = {
|
|
@@ -144,8 +145,12 @@ def _identity_error(value: object, field: str) -> str | None:
|
|
|
144
145
|
|
|
145
146
|
def _candidate_error(value: object, index: int) -> str | None:
|
|
146
147
|
field = f"candidates[{index}]"
|
|
147
|
-
|
|
148
|
+
from selector_credits import OPTIONAL_CANDIDATE_FIELDS, credit_candidate_error
|
|
149
|
+
if not isinstance(value, dict) or not CANDIDATE_KEYS <= set(value) \
|
|
150
|
+
or set(value) - CANDIDATE_KEYS - OPTIONAL_CANDIDATE_FIELDS:
|
|
148
151
|
return field
|
|
152
|
+
if error := credit_candidate_error(value, field):
|
|
153
|
+
return error
|
|
149
154
|
for name in ("runner_id", "runner_generation"):
|
|
150
155
|
item = value[name]
|
|
151
156
|
if not isinstance(item, int) or isinstance(item, bool) or not 0 < item <= SAFE_INTEGER:
|
|
@@ -186,6 +191,16 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
186
191
|
keys = set(request)
|
|
187
192
|
if not REQUEST_KEYS <= keys or keys - REQUEST_KEYS - OPTIONAL_REQUEST_KEYS:
|
|
188
193
|
return "invalid_request", {"field": "$"}
|
|
194
|
+
if "credit_fallback" in request and not isinstance(request["credit_fallback"], bool):
|
|
195
|
+
return "invalid_request", {"field": "credit_fallback"}
|
|
196
|
+
if "required_funding_tier" in request and (not isinstance(request["required_funding_tier"], str)
|
|
197
|
+
or request["required_funding_tier"] not in {"included", "reserve", "credits"}):
|
|
198
|
+
return "invalid_request", {"field": "required_funding_tier"}
|
|
199
|
+
if "required_funding_tier" in request and request.get("credit_fallback") is not True:
|
|
200
|
+
return "invalid_request", {"field": "required_funding_tier"}
|
|
201
|
+
age = request.get("credit_max_age_seconds", 900)
|
|
202
|
+
if isinstance(age, bool) or not isinstance(age, int) or not 30 <= age <= 3600:
|
|
203
|
+
return "invalid_request", {"field": "credit_max_age_seconds"}
|
|
189
204
|
for knob in ("headroom_band", "session_gate"):
|
|
190
205
|
if knob not in request:
|
|
191
206
|
continue
|
|
@@ -224,6 +239,9 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
224
239
|
if not isinstance(request["candidates"], list):
|
|
225
240
|
return "invalid_request", {"field": "candidates"}
|
|
226
241
|
for index, value in enumerate(request["candidates"]):
|
|
242
|
+
if isinstance(value, dict) and set(value) - CANDIDATE_KEYS \
|
|
243
|
+
and request.get("credit_fallback") is not True:
|
|
244
|
+
return "invalid_request", {"field": f"candidates[{index}]"}
|
|
227
245
|
if error := _candidate_error(value, index):
|
|
228
246
|
return "invalid_request", {"field": error}
|
|
229
247
|
return None
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Pure contract checks for spendable credit telemetry, separate from earned resets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "lib"))
|
|
8
|
+
from codex_credits import collect_credits, credit_view, failed_credits, snapshot_from_usage
|
|
9
|
+
|
|
10
|
+
NOW = 1791115200
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def payload(**updates):
|
|
14
|
+
return {"account_id": "account-test", "credits": {
|
|
15
|
+
"has_credits": True, "unlimited": False, "balance": "1234.5000"},
|
|
16
|
+
"rate_limit": {"allowed": False}, "spend_control_reached": False, **updates}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpendableCreditsTest(unittest.TestCase):
|
|
20
|
+
def test_provider_balance_and_identity_are_preserved_without_float_rounding(self):
|
|
21
|
+
raw = payload()
|
|
22
|
+
raw["credits"]["balance"] = "12345678901234567890.123456789"
|
|
23
|
+
result = snapshot_from_usage(raw, "account-test", NOW)
|
|
24
|
+
self.assertEqual(result["balance"], "12345678901234567890.123456789")
|
|
25
|
+
self.assertEqual(result["account_id"], "account-test")
|
|
26
|
+
self.assertEqual(result["fetched_at"], "2026-10-04T12:00:00Z")
|
|
27
|
+
self.assertIs(result["ordinary_usage_allowed"], False)
|
|
28
|
+
self.assertIs(result["spend_control_reached"], False)
|
|
29
|
+
|
|
30
|
+
def test_measured_zero_unlimited_and_unknown_balance_remain_distinct(self):
|
|
31
|
+
for raw, expected in (({"has_credits": False, "unlimited": False, "balance": "0.00"}, "0"),
|
|
32
|
+
({"has_credits": True, "unlimited": True, "balance": None}, None),
|
|
33
|
+
({"has_credits": True, "unlimited": False}, None)):
|
|
34
|
+
with self.subTest(raw=raw):
|
|
35
|
+
result = snapshot_from_usage(payload(credits=raw), "account-test", NOW)
|
|
36
|
+
self.assertEqual(result["balance"], expected)
|
|
37
|
+
self.assertEqual(result["unlimited"], raw["unlimited"])
|
|
38
|
+
|
|
39
|
+
def test_earned_resets_never_become_a_spendable_balance(self):
|
|
40
|
+
self.assertIsNone(collect_credits({"rate_limit_reset_credits": {"available_count": 3}},
|
|
41
|
+
"account-test", NOW))
|
|
42
|
+
self.assertIsNone(collect_credits({"credits": None}, "account-test", NOW))
|
|
43
|
+
|
|
44
|
+
def test_malformed_balances_and_truthy_flags_do_not_claim_available_credit(self):
|
|
45
|
+
for invalid in (True, 12.34, -1, "-1", "NaN", "Infinity", "1e3", "1,000", "9" * 129):
|
|
46
|
+
with self.subTest(invalid=invalid):
|
|
47
|
+
raw = payload()
|
|
48
|
+
raw["credits"]["balance"] = invalid
|
|
49
|
+
self.assertIsNone(collect_credits(raw, "account-test", NOW))
|
|
50
|
+
raw = payload()
|
|
51
|
+
raw["credits"]["has_credits"] = "true"
|
|
52
|
+
self.assertIsNone(collect_credits(raw, "account-test", NOW))
|
|
53
|
+
|
|
54
|
+
def test_response_identity_must_agree_with_authenticated_account(self):
|
|
55
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
56
|
+
bad = collect_credits(payload(account_id="other-account"), "account-test", NOW + 10, previous)
|
|
57
|
+
self.assertEqual(bad["account_id"], "account-test")
|
|
58
|
+
self.assertEqual(bad["last_error"], "account_mismatch")
|
|
59
|
+
self.assertEqual(bad["fetched_at"], previous["fetched_at"])
|
|
60
|
+
no_response_id = snapshot_from_usage(payload(account_id=None), "account-test", NOW)
|
|
61
|
+
self.assertEqual(no_response_id["account_id"], "account-test")
|
|
62
|
+
|
|
63
|
+
def test_failure_preserves_last_observation_then_success_clears_it(self):
|
|
64
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
65
|
+
failed = failed_credits(previous, "http_429", NOW + 10)
|
|
66
|
+
self.assertEqual(failed["balance"], "1234.5")
|
|
67
|
+
self.assertEqual(failed["fetched_at"], previous["fetched_at"])
|
|
68
|
+
self.assertTrue(failed["stale"])
|
|
69
|
+
self.assertEqual(failed["last_error"], "http_429")
|
|
70
|
+
missing = collect_credits({}, "account-test", NOW + 20, previous)
|
|
71
|
+
self.assertEqual(missing["last_error"], "missing_credits")
|
|
72
|
+
recovered = collect_credits(payload(), "account-test", NOW + 30, failed)
|
|
73
|
+
self.assertNotIn("last_error", recovered)
|
|
74
|
+
self.assertFalse(credit_view(recovered, NOW + 30)["stale"])
|
|
75
|
+
|
|
76
|
+
def test_staleness_uses_credit_timestamp_and_rejects_future_or_invalid_time(self):
|
|
77
|
+
value = snapshot_from_usage(payload(), "account-test", NOW)
|
|
78
|
+
self.assertFalse(credit_view(value, NOW + 900)["stale"])
|
|
79
|
+
self.assertTrue(credit_view(value, NOW + 901)["stale"])
|
|
80
|
+
self.assertTrue(credit_view(value, NOW - 31)["stale"])
|
|
81
|
+
self.assertIsNone(credit_view({**value, "fetched_at": "2026-10-04T12:00:00"}, NOW))
|
|
82
|
+
self.assertIsNone(credit_view({**value, "fetched_at": {}}, NOW))
|
|
83
|
+
|
|
84
|
+
def test_soft_failure_never_erases_a_known_auth_or_identity_failure(self):
|
|
85
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
86
|
+
for error in ("unauthenticated", "account_mismatch"):
|
|
87
|
+
failed = failed_credits(previous, error, NOW + 10)
|
|
88
|
+
retry = failed_credits(failed, "network_unreachable", NOW + 20)
|
|
89
|
+
self.assertEqual(retry["last_error"], error)
|
|
90
|
+
self.assertEqual(retry["last_error_at"], failed["last_error_at"])
|
|
91
|
+
self.assertNotIn("last_error", collect_credits(payload(), "account-test", NOW + 30, retry))
|
|
92
|
+
|
|
93
|
+
def test_transport_failure_diagnostics_preserve_paid_balance_and_its_timestamp(self):
|
|
94
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
95
|
+
for error in ("network_unreachable", "dns_unavailable", "usage_timeout", "http_401", "http_403"):
|
|
96
|
+
failed = failed_credits(previous, error, NOW + 10)
|
|
97
|
+
self.assertEqual(failed["last_error"], error)
|
|
98
|
+
self.assertEqual(failed["balance"], previous["balance"])
|
|
99
|
+
self.assertEqual(failed["fetched_at"], previous["fetched_at"])
|
|
100
|
+
|
|
101
|
+
def test_missing_credit_schema_does_not_hide_identity_mismatch(self):
|
|
102
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
103
|
+
result = collect_credits({"account_id": "other"}, "account-test", NOW + 10, previous)
|
|
104
|
+
self.assertEqual(result["last_error"], "account_mismatch")
|
|
105
|
+
|
|
106
|
+
def test_missing_balance_keeps_money_but_obeys_new_spend_control(self):
|
|
107
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
108
|
+
for raw in (payload(credits={"has_credits": True, "unlimited": False, "balance": None}),
|
|
109
|
+
payload(credits=None)):
|
|
110
|
+
raw["spend_control_reached"] = True
|
|
111
|
+
result = collect_credits(raw, "account-test", NOW + 10, previous)
|
|
112
|
+
self.assertEqual(result["balance"], previous["balance"])
|
|
113
|
+
self.assertEqual(result["fetched_at"], previous["fetched_at"])
|
|
114
|
+
self.assertTrue(result["spend_control_reached"])
|
|
115
|
+
self.assertIn(result["last_error"], ("invalid_credits", "missing_credits"))
|
|
116
|
+
|
|
117
|
+
def test_partial_read_never_clears_previous_spending_restrictions(self):
|
|
118
|
+
previous = snapshot_from_usage(payload(spend_control_reached=True,
|
|
119
|
+
rate_limit_reached_type="workspace_member_usage_limit_reached"), "account-test", NOW)
|
|
120
|
+
raw = payload(credits=None, spend_control_reached=False, rate_limit_reached_type="rate_limit_reached")
|
|
121
|
+
result = collect_credits(raw, "account-test", NOW + 10, previous)
|
|
122
|
+
self.assertTrue(result["spend_control_reached"])
|
|
123
|
+
self.assertEqual(result["limit_reached_type"], "workspace_member_usage_limit_reached")
|
|
124
|
+
self.assertFalse(collect_credits(payload(), "account-test", NOW + 20, result)["spend_control_reached"])
|
|
125
|
+
|
|
126
|
+
def test_admission_facts_are_never_inferred_from_percentages(self):
|
|
127
|
+
raw = payload(rate_limit={"primary_window": {"used_percent": 0}})
|
|
128
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
129
|
+
self.assertIsNone(snapshot["ordinary_usage_allowed"])
|
|
130
|
+
raw["spend_control_reached"] = "false"
|
|
131
|
+
raw["rate_limit_reached_type"] = {"unexpected": "shape"}
|
|
132
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
133
|
+
self.assertIsNone(snapshot["spend_control_reached"])
|
|
134
|
+
self.assertEqual(snapshot["limit_reached_type"], "unknown")
|
|
135
|
+
|
|
136
|
+
def test_documented_raw_and_app_server_nested_admission_fields(self):
|
|
137
|
+
raw = payload(spend_control={"reached": True},
|
|
138
|
+
rate_limit_reached_type={"type": "workspace_member_usage_limit_reached"})
|
|
139
|
+
raw.pop("spend_control_reached")
|
|
140
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
141
|
+
self.assertTrue(snapshot["spend_control_reached"])
|
|
142
|
+
self.assertEqual(snapshot["limit_reached_type"], "workspace_member_usage_limit_reached")
|
|
143
|
+
app_server = {"accountId": "account-test", "ordinaryUsageAllowed": False, "rateLimits": {
|
|
144
|
+
"credits": {"hasCredits": True, "unlimited": False, "balance": "12.34"},
|
|
145
|
+
"rateLimitReachedType": "rate_limit_reached", "spendControlReached": False}}
|
|
146
|
+
snapshot = snapshot_from_usage(app_server, "account-test", NOW)
|
|
147
|
+
self.assertEqual(snapshot["limit_reached_type"], "rate_limit_reached")
|
|
148
|
+
self.assertEqual(snapshot["balance"], "12.34")
|
|
149
|
+
self.assertIs(snapshot["ordinary_usage_allowed"], False)
|
|
150
|
+
self.assertIs(snapshot["spend_control_reached"], False)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
unittest.main()
|