@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 +6 -0
- package/dist/types/fetch-retry.d.ts +2 -1
- package/package.json +2 -2
- package/src/fetch-retry.ts +38 -7
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.
|
|
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.
|
|
57
|
+
"@oh-my-pi/pi-natives": "18.1.22"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "^1.3.14"
|
package/src/fetch-retry.ts
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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 {
|