@@ -94,6 +94,9 @@ def _session_total_tokens(entries: list[UsageEntry]) -> int:
9494
9595
9696def load_rate_limits () -> CodexRateLimits | None :
97+ sqlite_limits = _load_sqlite_rate_limits ()
98+ if sqlite_limits is not None :
99+ return sqlite_limits
97100 if not SESSIONS_DIR .is_dir ():
98101 return None
99102 models = _load_thread_models ()
@@ -105,6 +108,132 @@ def load_rate_limits() -> CodexRateLimits | None:
105108 return None
106109
107110
111+ def _load_sqlite_rate_limits () -> CodexRateLimits | None :
112+ if not LOGS_DB .exists ():
113+ return None
114+ query = (
115+ "SELECT ts, feedback_log_body FROM logs "
116+ "WHERE target = 'codex_api::endpoint::responses_websocket' "
117+ "AND feedback_log_body LIKE '%websocket event:%' "
118+ "AND (feedback_log_body LIKE '%\" type\" :\" codex.rate_limits\" %' "
119+ "OR feedback_log_body LIKE '%\" type\" :\" error\" %usage_limit_reached%') "
120+ "ORDER BY ts DESC, ts_nanos DESC, id DESC LIMIT 50"
121+ )
122+ try :
123+ with sqlite3 .connect (f"file:{ LOGS_DB } ?mode=ro" , uri = True ) as conn :
124+ rows = conn .execute (query ).fetchall ()
125+ except (OSError , sqlite3 .Error ):
126+ if os .environ .get ("USAGE_DEBUG" ) == "1" :
127+ logger .warning ("codex sqlite rate limits load failed" , exc_info = True )
128+ return None
129+
130+ for ts , body in rows :
131+ parsed = _parse_sqlite_rate_limits_row (ts , body )
132+ if parsed is not None :
133+ return parsed
134+ return None
135+
136+
137+ def _parse_sqlite_rate_limits_row (ts : Any , body : Any ) -> CodexRateLimits | None :
138+ if not isinstance (body , str ):
139+ return None
140+ event = _websocket_event_payload (body )
141+ if not event :
142+ return None
143+ if event .get ("type" ) == "codex.rate_limits" :
144+ return _rate_limits_from_websocket_event (event , body , ts )
145+ if event .get ("type" ) == "error" :
146+ return _rate_limits_from_websocket_error (event , body , ts )
147+ return None
148+
149+
150+ def _websocket_event_payload (body : str ) -> dict [str , Any ]:
151+ marker = "websocket event: "
152+ index = body .find (marker )
153+ if index < 0 :
154+ return {}
155+ try :
156+ data = json .loads (body [index + len (marker ):])
157+ except json .JSONDecodeError :
158+ return {}
159+ return data if isinstance (data , dict ) else {}
160+
161+
162+ def _rate_limits_from_websocket_event (
163+ event : dict [str , Any ],
164+ body : str ,
165+ ts : Any ,
166+ ) -> CodexRateLimits | None :
167+ rate_limits = _as_dict (event .get ("rate_limits" ))
168+ primary = _as_dict (rate_limits .get ("primary" ))
169+ secondary = _as_dict (rate_limits .get ("secondary" ))
170+ return _build_rate_limits (
171+ primary_pct = _as_optional_float (primary .get ("used_percent" )),
172+ primary_reset = _as_optional_float (primary .get ("reset_at" )),
173+ secondary_pct = _as_optional_float (secondary .get ("used_percent" )),
174+ secondary_reset = _as_optional_float (secondary .get ("reset_at" )),
175+ model = _event_value (body , "model" ) or "unknown" ,
176+ updated_at = _timestamp_from_log_ts (ts ),
177+ )
178+
179+
180+ def _rate_limits_from_websocket_error (
181+ event : dict [str , Any ],
182+ body : str ,
183+ ts : Any ,
184+ ) -> CodexRateLimits | None :
185+ headers = _as_dict (event .get ("headers" ))
186+ primary_reset = _as_optional_float (headers .get ("X-Codex-Primary-Reset-At" ))
187+ secondary_reset = _as_optional_float (headers .get ("X-Codex-Secondary-Reset-At" ))
188+ now_ts = datetime .now (UTC ).timestamp ()
189+ if primary_reset is None :
190+ primary_reset_after = _as_optional_float (headers .get ("X-Codex-Primary-Reset-After-Seconds" ))
191+ primary_reset = now_ts + primary_reset_after if primary_reset_after is not None else None
192+ if secondary_reset is None :
193+ secondary_reset_after = _as_optional_float (
194+ headers .get ("X-Codex-Secondary-Reset-After-Seconds" )
195+ )
196+ secondary_reset = (
197+ now_ts + secondary_reset_after if secondary_reset_after is not None else None
198+ )
199+ return _build_rate_limits (
200+ primary_pct = _as_optional_float (headers .get ("X-Codex-Primary-Used-Percent" )),
201+ primary_reset = primary_reset ,
202+ secondary_pct = _as_optional_float (headers .get ("X-Codex-Secondary-Used-Percent" )),
203+ secondary_reset = secondary_reset ,
204+ model = _event_value (body , "model" ) or "unknown" ,
205+ updated_at = _timestamp_from_log_ts (ts ),
206+ )
207+
208+
209+ def _build_rate_limits (
210+ * ,
211+ primary_pct : float | None ,
212+ primary_reset : float | None ,
213+ secondary_pct : float | None ,
214+ secondary_reset : float | None ,
215+ model : str ,
216+ updated_at : datetime | None ,
217+ ) -> CodexRateLimits | None :
218+ now_ts = datetime .now (UTC ).timestamp ()
219+ if primary_reset is not None and primary_reset < now_ts :
220+ primary_pct = None
221+ primary_reset = None
222+ if secondary_reset is not None and secondary_reset < now_ts :
223+ secondary_pct = None
224+ secondary_reset = None
225+ if primary_pct is None and secondary_pct is None :
226+ return None
227+ return CodexRateLimits (
228+ five_hour_pct = primary_pct ,
229+ five_hour_resets_at = primary_reset ,
230+ seven_day_pct = secondary_pct ,
231+ seven_day_resets_at = secondary_reset ,
232+ model = model ,
233+ updated_at = updated_at .isoformat () if updated_at is not None else "" ,
234+ )
235+
236+
108237def _load_thread_models () -> dict [str , str ]:
109238 return {
110239 thread_id : metadata .model
@@ -225,7 +354,7 @@ def _parse_sqlite_log_row(
225354def _event_value (body : str , key : str ) -> str :
226355 pattern = _EVENT_VALUE_RE_CACHE .get (key )
227356 if pattern is None :
228- pattern = re .compile (rf'(?:^|\s ){ re .escape (key )} =(?:"([^"]*)"|([^\s]+))' )
357+ pattern = re .compile (rf'(?:^|[\s{{] ){ re .escape (key )} =(?:"([^"]*)"|([^\s}} ]+))' )
229358 _EVENT_VALUE_RE_CACHE [key ] = pattern
230359 match = pattern .search (body )
231360 if match is None :
0 commit comments