@oh-my-pi/pi-utils 18.1.21 → 18.1.22

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.22] - 2026-09-14
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `extractRetryHint` sleeping hours past the provider's stated wait when a timezone-naive `reset at` timestamp overshoots the relative retry hint: the skewed stamp is now ignored instead of winning longest-wins ([#12070](https://github.com/can1357/oh-my-pi/pull/12070) by [@H4vC](https://github.com/H4vC)).
10
+
5
11
  ## [18.1.21] - 2026-09-14
6
12
 
7
13
  ### Added
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Server-suggested retry delay extraction. Merges the patterns historically used
3
- * by the OpenAI Codex and Google Gemini retry helpers.
4
3
  *
5
4
  * Header sources (checked in order):
6
5
  * - `retry-after-ms` (milliseconds)
@@ -16,6 +15,8 @@
16
15
  * - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
17
16
  * - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
18
17
  * - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
18
+ * (offset-bearing only; a timezone-naive stamp is provider wall clock in
19
+ * an unknown zone and only resolves when no relative signal is present)
19
20
  *
20
21
  * Returns `undefined` if no signal is found, or `0` when the provider
21
22
  * explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.1.21",
3
+ "version": "18.1.22",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -54,7 +54,7 @@
54
54
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
55
55
  },
56
56
  "dependencies": {
57
- "@oh-my-pi/pi-natives": "18.1.21"
57
+ "@oh-my-pi/pi-natives": "18.1.22"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.3.14"
@@ -19,9 +19,18 @@ const CN_RESET_AT_PATTERN = /将在\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-
19
19
  // "retry-after-ms=98497000" / "retry-after-ms: 7200000" / "retry-after-ms = 7200000"
20
20
  const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
21
21
 
22
+ // A timezone-naive `reset at` stamp (no `Z`/offset) is the provider's
23
+ // wall clock in an unknown zone: it cannot be converted to a delay without
24
+ // guessing the zone, so it resolves only as a fallback when the body
25
+ // carries no unambiguous relative signal.
26
+
27
+ // A conflict probe needs no new signal: when a naive `reset at` wall stamp
28
+ // disagrees with the merged relative wait, the merged wait (which already
29
+ // ignores the naive stamp) sleeps first, and the retry after it is the
30
+ // probe — success proves skew, a fresh 429 re-anchors with live timing.
31
+
22
32
  /**
23
33
  * Server-suggested retry delay extraction. Merges the patterns historically used
24
- * by the OpenAI Codex and Google Gemini retry helpers.
25
34
  *
26
35
  * Header sources (checked in order):
27
36
  * - `retry-after-ms` (milliseconds)
@@ -37,6 +46,8 @@ const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
37
46
  * - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
38
47
  * - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
39
48
  * - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
49
+ * (offset-bearing only; a timezone-naive stamp is provider wall clock in
50
+ * an unknown zone and only resolves when no relative signal is present)
40
51
  *
41
52
  * Returns `undefined` if no signal is found, or `0` when the provider
42
53
  * explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
@@ -98,6 +109,15 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
98
109
  // when the parse returns undefined, which would sleep a session the
99
110
  // provider told to retry immediately.
100
111
  let retryNow = false;
112
+
113
+ // Timezone-naive `reset at` stamps (no `Z`/offset) are the provider's
114
+ // wall clock in an unknown zone — converting them to a delay requires
115
+ // guessing the zone. They resolve AFTER every unambiguous signal below,
116
+ // and only as a fallback when none was found.
117
+ let longestNaiveMs: number | undefined;
118
+ const considerNaive = (ms: number | undefined): void => {
119
+ if (ms !== undefined && ms > 0 && (longestNaiveMs === undefined || ms > longestNaiveMs)) longestNaiveMs = ms;
120
+ };
101
121
  const consider = (ms: number | undefined): void => {
102
122
  if (ms !== undefined && ms > 0 && (longestMs === undefined || ms > longestMs)) longestMs = ms;
103
123
  };
@@ -119,14 +139,23 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
119
139
  }
120
140
  for (const pattern of [WILL_RESET_AT_PATTERN, CN_RESET_AT_PATTERN]) {
121
141
  const match = pattern.exec(body);
122
- if (match?.[1]) {
123
- // Provider timestamps without an explicit offset are interpreted as UTC.
124
- const normalized = match[1].replace(" ", "T");
125
- const hasOffset = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized);
126
- const parsed = Date.parse(hasOffset ? normalized : `${normalized}Z`);
142
+ if (!match?.[1]) continue;
143
+ // Offset-bearing stamps are unambiguous and compete by longest-wins.
144
+ // Naive stamps (provider wall clock, unknown zone) resolve after the
145
+ // relative signals below, and only when nothing unambiguous was
146
+ // found — never by guessing the zone against a conflicting signal.
147
+ const normalized = match[1].replace(" ", "T");
148
+ const hasOffset = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized);
149
+ if (hasOffset) {
150
+ const parsed = Date.parse(normalized);
127
151
  if (!Number.isNaN(parsed) && parsed > Date.now()) {
128
152
  consider(parsed - Date.now());
129
153
  }
154
+ } else {
155
+ const parsed = Date.parse(`${normalized}Z`);
156
+ if (!Number.isNaN(parsed) && parsed > Date.now()) {
157
+ considerNaive(parsed - Date.now());
158
+ }
130
159
  }
131
160
  }
132
161
  const accountResetMatch = WILL_RESET_IN_PATTERN.exec(body);
@@ -188,7 +217,9 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
188
217
  considerClamped(resetSeconds > 1_000_000_000 ? resetSeconds * 1000 - Date.now() : resetSeconds * 1000);
189
218
  }
190
219
  }
191
- return longestMs ?? (retryNow ? 0 : undefined);
220
+ // Elapsed naive stamps were ignored before this change and stay ignored:
221
+ // only an explicit zero/expired relative signal is authoritative retry-now.
222
+ return longestMs ?? longestNaiveMs ?? (retryNow ? 0 : undefined);
192
223
  }
193
224
 
194
225
  function unitToMs(unit: string): number | undefined {