@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.
@@ -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/usage poll) may be buffered.
634
-
635
- def _is_oauth_usage_flow(self, flow: http.HTTPFlow) -> bool:
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] == self._OAUTH_USAGE_PATH
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._is_oauth_usage_flow(flow)
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 carries the rate-limit tier
1046
- # (e.g. default_claude_max_5x) that the usage body does NOT — cache
1047
- # it so usage observations get a real plan instead of 'unknown'.
1048
- if path == "/api/oauth/account/settings" and flow.response is not None \
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
- tier = _extract_plan_tier(json.loads(flow.response.get_text() or "null"))
1082
+ parsed = json.loads(body_text or "null")
1052
1083
  except (json.JSONDecodeError, ValueError):
1053
- tier = None
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulemetric/proxy",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"