@bitkyc08/opencodex 2.58.0 → 2.59.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.
- package/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** A response-body guard owns one reader, never the request-wide abort controller. */
|
|
2
|
+
export interface ResponseBodyInactivityGuard {
|
|
3
|
+
response: Response;
|
|
4
|
+
/** Stop an abandoned parser without waiting for a broken source's cancel promise. */
|
|
5
|
+
dispose: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ResponseBodyInactivityError extends DOMException {
|
|
9
|
+
constructor(timeoutMs: number) {
|
|
10
|
+
super(`Upstream response body stalled for ${timeoutMs}ms`, "TimeoutError");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bound silence only while the consumer is waiting for upstream bytes.
|
|
16
|
+
*
|
|
17
|
+
* A zero high-water mark prevents this wrapper from speculatively reading while
|
|
18
|
+
* the consumer is paused. Empty chunks neither count as progress nor reset the
|
|
19
|
+
* deadline. Cancellation is local to this body: aborting a shared request signal
|
|
20
|
+
* here would suppress the failure terminal in the enclosing Responses bridge.
|
|
21
|
+
*/
|
|
22
|
+
export function guardResponseBodyInactivity(
|
|
23
|
+
response: Response,
|
|
24
|
+
signal: AbortSignal | undefined,
|
|
25
|
+
timeoutMs: number,
|
|
26
|
+
): ResponseBodyInactivityGuard {
|
|
27
|
+
const source = response.body;
|
|
28
|
+
if (!source) return { response, dispose: () => {} };
|
|
29
|
+
|
|
30
|
+
const reader = source.getReader();
|
|
31
|
+
const timeoutEnabled = Number.isFinite(timeoutMs) && timeoutMs > 0;
|
|
32
|
+
const timeoutError = new ResponseBodyInactivityError(timeoutMs);
|
|
33
|
+
let output!: ReadableStreamDefaultController<Uint8Array>;
|
|
34
|
+
let settled = false;
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
let deadline: number | undefined;
|
|
37
|
+
|
|
38
|
+
const pause = (): void => {
|
|
39
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
40
|
+
timer = undefined;
|
|
41
|
+
deadline = undefined;
|
|
42
|
+
};
|
|
43
|
+
const release = (): void => {
|
|
44
|
+
try { reader.releaseLock(); } catch { /* a source may already have torn down */ }
|
|
45
|
+
};
|
|
46
|
+
const settle = (): boolean => {
|
|
47
|
+
if (settled) return false;
|
|
48
|
+
settled = true;
|
|
49
|
+
pause();
|
|
50
|
+
signal?.removeEventListener("abort", onAbort);
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const cancelSource = (reason?: unknown): void => {
|
|
54
|
+
// reader.cancel settles outstanding reads before its source cleanup completes.
|
|
55
|
+
// Never await that cleanup; custom transports can leave it pending forever.
|
|
56
|
+
try { void reader.cancel(reason).catch(() => {}); } catch { /* already released */ }
|
|
57
|
+
release();
|
|
58
|
+
};
|
|
59
|
+
const fail = (reason: unknown): void => {
|
|
60
|
+
if (!settle()) return;
|
|
61
|
+
// Publish the failure before cancellation can synchronously tear down relays.
|
|
62
|
+
try { output.error(reason); } catch { /* downstream is already closed */ }
|
|
63
|
+
cancelSource(reason);
|
|
64
|
+
};
|
|
65
|
+
const onAbort = (): void => {
|
|
66
|
+
fail(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
|
|
67
|
+
};
|
|
68
|
+
const arm = (): void => {
|
|
69
|
+
if (settled || deadline === undefined) return;
|
|
70
|
+
// Avoid the platform's signed-32-bit setTimeout overflow for large settings.
|
|
71
|
+
const remaining = deadline - performance.now();
|
|
72
|
+
if (remaining <= 0) {
|
|
73
|
+
fail(timeoutError);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
timer = setTimeout(() => {
|
|
77
|
+
timer = undefined;
|
|
78
|
+
arm();
|
|
79
|
+
}, Math.min(2_147_483_647, Math.max(1, remaining)));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const body = new ReadableStream<Uint8Array>({
|
|
83
|
+
start(controller) {
|
|
84
|
+
output = controller;
|
|
85
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
if (signal?.aborted) onAbort();
|
|
87
|
+
},
|
|
88
|
+
async pull(controller) {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
if (timeoutEnabled) {
|
|
91
|
+
deadline = performance.now() + timeoutMs;
|
|
92
|
+
arm();
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
let emptyReads = 0;
|
|
96
|
+
for (;;) {
|
|
97
|
+
// A producer of immediately resolved empty chunks can starve timers.
|
|
98
|
+
// The monotonic deadline check also bounds that microtask-only loop.
|
|
99
|
+
if (deadline !== undefined && performance.now() >= deadline) {
|
|
100
|
+
fail(timeoutError);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (settled) return;
|
|
105
|
+
if (done) {
|
|
106
|
+
settle();
|
|
107
|
+
release();
|
|
108
|
+
controller.close();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (value.byteLength === 0) {
|
|
112
|
+
// Empty chunks are not progress, so the deadline keeps running. Hand the
|
|
113
|
+
// macrotask queue a turn periodically: otherwise a long configured timeout
|
|
114
|
+
// lets this microtask chain hold timers and unrelated requests.
|
|
115
|
+
emptyReads += 1;
|
|
116
|
+
if (emptyReads % 64 === 0) {
|
|
117
|
+
await new Promise<void>(resolve => { setTimeout(resolve, 0); });
|
|
118
|
+
if (settled) return;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
emptyReads = 0;
|
|
123
|
+
pause();
|
|
124
|
+
controller.enqueue(value);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
fail(error);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
cancel(reason) {
|
|
132
|
+
if (!settle()) return;
|
|
133
|
+
cancelSource(reason);
|
|
134
|
+
},
|
|
135
|
+
}, { highWaterMark: 0 });
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
response: new Response(body, {
|
|
139
|
+
status: response.status,
|
|
140
|
+
statusText: response.statusText,
|
|
141
|
+
headers: response.headers,
|
|
142
|
+
}),
|
|
143
|
+
dispose: () => {
|
|
144
|
+
if (!settle()) return;
|
|
145
|
+
try { output.close(); } catch { /* parser already settled */ }
|
|
146
|
+
cancelSource();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Native SSE and bounded JSON/error readers retain their existing ownership. */
|
|
152
|
+
export function guardDirectPassthroughBodyInactivity(
|
|
153
|
+
response: Response,
|
|
154
|
+
signal: AbortSignal | undefined,
|
|
155
|
+
timeoutMs: number,
|
|
156
|
+
): Response {
|
|
157
|
+
const redirect = response.status >= 300 && response.status < 400;
|
|
158
|
+
const contentType = response.headers.get("content-type")?.toLowerCase();
|
|
159
|
+
if (!redirect && (!response.ok
|
|
160
|
+
|| contentType?.includes("text/event-stream")
|
|
161
|
+
|| contentType?.includes("application/json"))) return response;
|
|
162
|
+
return guardResponseBodyInactivity(response, signal, timeoutMs).response;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Scope a buffered adapter parser, including throws and early successful returns. */
|
|
166
|
+
export async function readResponseBodyWithInactivity<T>(
|
|
167
|
+
response: Response,
|
|
168
|
+
signal: AbortSignal | undefined,
|
|
169
|
+
timeoutMs: number,
|
|
170
|
+
parse: (response: Response) => Promise<T>,
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
const guarded = guardResponseBodyInactivity(response, signal, timeoutMs);
|
|
173
|
+
try {
|
|
174
|
+
return await parse(guarded.response);
|
|
175
|
+
} finally {
|
|
176
|
+
guarded.dispose();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Scope an adapter stream, including continuation legs and iterator abandonment. */
|
|
181
|
+
export async function* readResponseStreamWithInactivity<T>(
|
|
182
|
+
response: Response,
|
|
183
|
+
signal: AbortSignal | undefined,
|
|
184
|
+
timeoutMs: number,
|
|
185
|
+
parse: (response: Response) => AsyncIterable<T>,
|
|
186
|
+
): AsyncGenerator<T> {
|
|
187
|
+
const guarded = guardResponseBodyInactivity(response, signal, timeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
yield* parse(guarded.response);
|
|
190
|
+
} finally {
|
|
191
|
+
guarded.dispose();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a provider's relative retry hint without mistaking a unit prefix for a
|
|
3
|
+
* complete unit. Kept independent of error classification and combo routing.
|
|
4
|
+
*/
|
|
5
|
+
const UNIT_SECONDS: Readonly<Record<string, number>> = {
|
|
6
|
+
ms: 0.001, msec: 0.001, msecs: 0.001, millisecond: 0.001, milliseconds: 0.001,
|
|
7
|
+
s: 1, sec: 1, secs: 1, second: 1, seconds: 1,
|
|
8
|
+
m: 60, min: 60, mins: 60, minute: 60, minutes: 60,
|
|
9
|
+
h: 3600, hr: 3600, hrs: 3600, hour: 3600, hours: 3600,
|
|
10
|
+
d: 86400, day: 86400, days: 86400,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Read the WHOLE word before looking it up. In particular, ms is not m and
|
|
14
|
+
// "months" must not accidentally become minutes. Digits may follow a unit so
|
|
15
|
+
// compact, unambiguous durations such as 1h30m remain supported.
|
|
16
|
+
const COMPONENT = /^(\d+(?:\.\d+)?)\s*([a-z]+)(?![a-z_])/i;
|
|
17
|
+
const SEPARATOR = /^(?:\s*,\s*(?:and\s+)?|\s+and\s+|\s*\+\s*|\s*)/i;
|
|
18
|
+
const MAX_COMPONENTS = 16;
|
|
19
|
+
|
|
20
|
+
function durationSeconds(tail: string, allowBareSeconds: boolean): number | undefined {
|
|
21
|
+
let rest = tail.trimStart();
|
|
22
|
+
let seconds = 0;
|
|
23
|
+
let components = 0;
|
|
24
|
+
while (true) {
|
|
25
|
+
const component = COMPONENT.exec(rest);
|
|
26
|
+
if (!component) {
|
|
27
|
+
if (components !== 0 || !allowBareSeconds) return undefined;
|
|
28
|
+
// A header-style bare number means seconds. Never salvage the numeric
|
|
29
|
+
// prefix of an unsupported unit, exponent, signed value or clock time.
|
|
30
|
+
const bare = /^(\d+(?:\.\d+)?)(?![\w.:+-])(?=\s*(?:$|[.,;!?)\]}]))/.exec(rest);
|
|
31
|
+
if (!bare) return undefined;
|
|
32
|
+
seconds = Number(bare[1]);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const unit = UNIT_SECONDS[component[2]!.toLowerCase()];
|
|
36
|
+
if (unit === undefined) return undefined;
|
|
37
|
+
seconds += Number(component[1]) * unit;
|
|
38
|
+
if (!Number.isFinite(seconds) || ++components > MAX_COMPONENTS) return undefined;
|
|
39
|
+
rest = rest.slice(component[0].length);
|
|
40
|
+
const separator = SEPARATOR.exec(rest)![0];
|
|
41
|
+
const next = rest.slice(separator.length);
|
|
42
|
+
if (!/^[+-]?(?:\d|\.\d)/.test(next)) break;
|
|
43
|
+
// A numeric continuation is part of this duration; a malformed second
|
|
44
|
+
// component must reject the hint, not silently shorten it to the first.
|
|
45
|
+
rest = next;
|
|
46
|
+
}
|
|
47
|
+
const rounded = Math.ceil(seconds);
|
|
48
|
+
return Number.isSafeInteger(rounded) && rounded > 0 ? rounded : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Supports reset(s) in, try again in and Retry-After/retry after hints; accepts
|
|
53
|
+
* compound durations and rounds UP once after summing all components.
|
|
54
|
+
* A bare number is permitted only for header-style Retry-After hints, never
|
|
55
|
+
* for "reset in 2026". When a message declares several usable lower bounds,
|
|
56
|
+
* honour the longest one rather than re-entering a still-live quota window.
|
|
57
|
+
*/
|
|
58
|
+
export function parseRetryAfterFromMessage(message: string): number | undefined {
|
|
59
|
+
const hints = /\b(try\s+again\s+in|retry[- ]after|resets?\s+in)\s*:?\s*/gi;
|
|
60
|
+
let result: number | undefined;
|
|
61
|
+
for (const hint of message.matchAll(hints)) {
|
|
62
|
+
const seconds = durationSeconds(
|
|
63
|
+
message.slice(hint.index! + hint[0].length),
|
|
64
|
+
/^retry[- ]after$/i.test(hint[1]!),
|
|
65
|
+
);
|
|
66
|
+
if (seconds !== undefined) result = Math.max(result ?? 0, seconds);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|