@rulemetric/proxy 0.8.0 → 0.10.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/addon/rulemetric_addon.py +66 -11
- package/package.json +1 -1
|
@@ -264,6 +264,12 @@ _PENDING_LOCK = threading.Lock()
|
|
|
264
264
|
# moments of proxy start while Claude Code is in use.
|
|
265
265
|
_CACHED_PLAN: dict[str, str | None] = {"plan": None}
|
|
266
266
|
|
|
267
|
+
# One-shot guard for the "settings carried no tier" warning (dict so tests can
|
|
268
|
+
# reset). Verified 2026-08-12: for at least some accounts Anthropic sends
|
|
269
|
+
# `internal_tier_rate_limit_tier: null`, so this is a steady state, not a
|
|
270
|
+
# transient — the warning must not fire on every poll.
|
|
271
|
+
_SETTINGS_DRIFT_LOGGED: dict[str, bool] = {"done": False}
|
|
272
|
+
|
|
267
273
|
_TIER_MAP = (
|
|
268
274
|
("max_20x", "max_20x"),
|
|
269
275
|
("max_5x", "max_5x"),
|
|
@@ -630,20 +636,30 @@ class RuleMetricAddon:
|
|
|
630
636
|
# (last exit reason OS_REASON_JETSAM, 200+ restarts/day), which is why
|
|
631
637
|
# the /usage dashboard went stale: the proxy was dead more often than
|
|
632
638
|
# alive and the gateway routed direct. Only flows we actually parse
|
|
633
|
-
# (detected LLM providers + the oauth
|
|
634
|
-
|
|
635
|
-
|
|
639
|
+
# (detected LLM providers + the oauth side-channel polls) may be buffered.
|
|
640
|
+
#
|
|
641
|
+
# Both oauth paths must be listed here, not just the usage poll. The guard
|
|
642
|
+
# streams anything `_flow_is_parsed()` rejects, and a streamed response has
|
|
643
|
+
# no body — so omitting `account/settings` silently disabled plan detection
|
|
644
|
+
# entirely: the endpoint reached the proxy 73 times and produced 0
|
|
645
|
+
# detections, leaving every observation stamped plan='unknown' and
|
|
646
|
+
# /usage/inferred-caps falling back to Max-20x for every user (found
|
|
647
|
+
# 2026-08-12). Buffering these two is safe — they are small JSON metadata
|
|
648
|
+
# responses, not the CDN downloads and telemetry payloads that caused the
|
|
649
|
+
# jetsam crash loop.
|
|
650
|
+
|
|
651
|
+
def _is_oauth_side_channel_flow(self, flow: http.HTTPFlow) -> bool:
|
|
636
652
|
return (
|
|
637
653
|
flow.request.method == "GET"
|
|
638
654
|
and flow.request.pretty_host == self._OAUTH_USAGE_HOST
|
|
639
|
-
and flow.request.path.split("?", 1)[0]
|
|
655
|
+
and flow.request.path.split("?", 1)[0] in self._OAUTH_PARSED_PATHS
|
|
640
656
|
)
|
|
641
657
|
|
|
642
658
|
def _flow_is_parsed(self, flow: http.HTTPFlow) -> bool:
|
|
643
659
|
"""True when request/response bodies are needed by this addon."""
|
|
644
660
|
if detect_provider(flow.request.pretty_host, flow.request.path) is not None:
|
|
645
661
|
return True
|
|
646
|
-
return self.
|
|
662
|
+
return self._is_oauth_side_channel_flow(flow)
|
|
647
663
|
|
|
648
664
|
def requestheaders(self, flow: http.HTTPFlow) -> None:
|
|
649
665
|
if not self._flow_is_parsed(flow):
|
|
@@ -1026,6 +1042,11 @@ class RuleMetricAddon:
|
|
|
1026
1042
|
|
|
1027
1043
|
_OAUTH_USAGE_HOST = "api.anthropic.com"
|
|
1028
1044
|
_OAUTH_USAGE_PATH = "/api/oauth/usage"
|
|
1045
|
+
# Side channel carrying the rate-limit tier (the usage body has no plan
|
|
1046
|
+
# field). Must stay in _OAUTH_PARSED_PATHS or its body is streamed away
|
|
1047
|
+
# before _extract_plan_tier can read it — see the streaming guard above.
|
|
1048
|
+
_OAUTH_SETTINGS_PATH = "/api/oauth/account/settings"
|
|
1049
|
+
_OAUTH_PARSED_PATHS = (_OAUTH_USAGE_PATH, _OAUTH_SETTINGS_PATH)
|
|
1029
1050
|
_OAUTH_USAGE_WINDOWS = (
|
|
1030
1051
|
("five_hour", "five_hour_percent"),
|
|
1031
1052
|
("seven_day", "seven_day_percent"),
|
|
@@ -1042,15 +1063,49 @@ class RuleMetricAddon:
|
|
|
1042
1063
|
# Match exact path (strip query string just in case)
|
|
1043
1064
|
path = request.path.split("?", 1)[0]
|
|
1044
1065
|
|
|
1045
|
-
# Side channel: oauth/account/settings
|
|
1046
|
-
# (e.g. default_claude_max_5x)
|
|
1047
|
-
#
|
|
1048
|
-
|
|
1066
|
+
# Side channel: oauth/account/settings is the only response that has
|
|
1067
|
+
# ever carried a rate-limit tier (e.g. default_claude_max_5x); the
|
|
1068
|
+
# usage body has no plan field at all. When present, cache it so usage
|
|
1069
|
+
# observations get a real plan instead of 'unknown'.
|
|
1070
|
+
#
|
|
1071
|
+
# It is frequently ABSENT. Verified 2026-08-12 against a live account:
|
|
1072
|
+
# the field is there but null (`internal_tier_rate_limit_tier: None`,
|
|
1073
|
+
# alongside `internal_tier_seat_tier` and `internal_tier_org_type`,
|
|
1074
|
+
# also null). So buffering this response is necessary for detection but
|
|
1075
|
+
# not sufficient — for such accounts the plan cannot be read from the
|
|
1076
|
+
# wire at all, and /usage/inferred-caps keeps its assumed=true
|
|
1077
|
+
# fallback. The warning below is what makes that visible.
|
|
1078
|
+
if path == self._OAUTH_SETTINGS_PATH and flow.response is not None \
|
|
1049
1079
|
and flow.response.status_code == 200:
|
|
1080
|
+
body_text = flow.response.get_text()
|
|
1050
1081
|
try:
|
|
1051
|
-
|
|
1082
|
+
parsed = json.loads(body_text or "null")
|
|
1052
1083
|
except (json.JSONDecodeError, ValueError):
|
|
1053
|
-
|
|
1084
|
+
parsed = None
|
|
1085
|
+
tier = _extract_plan_tier(parsed)
|
|
1086
|
+
if tier is None and not _SETTINGS_DRIFT_LOGGED["done"]:
|
|
1087
|
+
# Once per process: Claude Code polls this endpoint on every
|
|
1088
|
+
# startup (77 hits in one log), and the tier is null for
|
|
1089
|
+
# accounts Anthropic does not populate it for — warning on
|
|
1090
|
+
# each would bury the log it is meant to make readable.
|
|
1091
|
+
_SETTINGS_DRIFT_LOGGED["done"] = True
|
|
1092
|
+
# Loud on purpose, mirroring the oauth/usage drift warning
|
|
1093
|
+
# below. A silently-absent tier is indistinguishable from a
|
|
1094
|
+
# never-buffered body, and that ambiguity is what hid this for
|
|
1095
|
+
# months — `buffered` disambiguates the two. Restricted to
|
|
1096
|
+
# `*tier*` fields: the rest of this body is account settings
|
|
1097
|
+
# and must never reach the log.
|
|
1098
|
+
tier_fields = (
|
|
1099
|
+
{k: v for k, v in parsed.items() if "tier" in k.replace("_", "").lower()}
|
|
1100
|
+
if isinstance(parsed, dict)
|
|
1101
|
+
else {}
|
|
1102
|
+
)
|
|
1103
|
+
logger.warning(
|
|
1104
|
+
"oauth/account/settings carried no rate-limit tier — "
|
|
1105
|
+
"schema drift? buffered=%s tier_fields=%r",
|
|
1106
|
+
body_text is not None,
|
|
1107
|
+
tier_fields,
|
|
1108
|
+
)
|
|
1054
1109
|
if tier and tier != _CACHED_PLAN["plan"]:
|
|
1055
1110
|
_CACHED_PLAN["plan"] = tier
|
|
1056
1111
|
logger.info("detected plan tier from account/settings: %s", tier)
|