ldrouter 1.17.6 → 1.17.7

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.17.7] - 2026-09-20
8
+
9
+ ### Fixed
10
+
11
+ - **Codex accounts were refreshed at the last minute, in the window where a rotating grant is most fragile.** The refresh lead is a threshold on the *remaining* lifetime, not a token lifetime — `needsCodexRefresh` fires once `expires - now` drops below it — and it sat at 5 minutes, so the exchange ran as the access token expired. Because OpenAI rotates the refresh token on every exchange, that is exactly when another client holding the same grant can invalidate it, and the observed fleet showed every account at `consecutive_failures = 0`: the first failure was already being treated as final. Codex access tokens last 10 days (measured from the fleet: a rotation at 13:39:00 expired at 13:39:00 ten days later), so the lead is now 5 days — half a lifetime of margin, refreshing every 5 days instead of every 10. This matches the 5-day `refreshLeadMs` 9router ships for Codex, and it surfaces a dead grant on day 5 rather than day 10. The lead is deliberately far below the lifetime: any value above it makes `needsCodexRefresh` permanently true and refreshes on *every* request, which multiplies refresh-token rotation by the request count.
12
+
7
13
  ## [1.17.6] - 2026-09-20
8
14
 
9
15
  ### Fixed
@@ -28,7 +28,12 @@ const PERMANENT_REFRESH_FAILURES = new Set(['oauth_refresh_failed', 'credential_
28
28
  export function isPermanentRefreshFailure(error) {
29
29
  return PERMANENT_REFRESH_FAILURES.has(error);
30
30
  }
31
- const REFRESH_LEAD_MS = 5 * 60 * 1000;
31
+ // Lead is measured against the remaining lifetime, and must stay well under it: a refresh that fires
32
+ // at the last minute races the expiry, and because OpenAI rotates the refresh token on every
33
+ // exchange, that is exactly the moment another client holding the same grant can invalidate it.
34
+ // Codex access tokens last 10 days, so a 5-day lead keeps half the lifetime as margin and refreshes
35
+ // every 5 days. Matches the 5-day `refreshLeadMs` 9router ships for Codex.
36
+ const REFRESH_LEAD_MS = 5 * 24 * 60 * 60 * 1000;
32
37
  const REFRESH_TIMEOUT_MS = 10_000;
33
38
  const flights = new Map();
34
39
  let refreshClient = defaultRefreshClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldrouter",
3
- "version": "1.17.6",
3
+ "version": "1.17.7",
4
4
  "description": "LateDev Router — lightweight self-hosted LLM gateway with admin UI",
5
5
  "type": "module",
6
6
  "license": "MIT",