@vymalo/opencode-ratelimit 0.11.0 → 0.14.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/dist/plugin.js CHANGED
@@ -1,461 +1,480 @@
1
1
  import { parseRateLimitOptions, selectTier } from "./config.js";
2
2
  import { parseRateLimit } from "./headers.js";
3
3
  /** Fallback backoff when a 429 carries no `x-ratelimit-reset` and no `Retry-After`. */
4
- export const DEFAULT_BACKOFF_MS = 1000;
4
+ export const DEFAULT_BACKOFF_MS = 1e3;
5
5
  export function createProviderState() {
6
- return { cooldownUntilMs: 0, cooldownMaxWaitMs: 0 };
6
+ return {
7
+ cooldownUntilMs: 0,
8
+ cooldownMaxWaitMs: 0
9
+ };
7
10
  }
8
11
  /**
9
- * Walk every provider in the assembled OpenCode config; for each one that has
10
- * opted in via `options.meta.rateLimit`, wrap its `options.fetch` with a
11
- * rate-limit-aware fetch. Synchronous — all real work happens lazily inside the
12
- * wrapper at request time.
13
- */
12
+ * Walk every provider in the assembled OpenCode config; for each one that has
13
+ * opted in via `options.meta.rateLimit`, wrap its `options.fetch` with a
14
+ * rate-limit-aware fetch. Synchronous — all real work happens lazily inside the
15
+ * wrapper at request time.
16
+ */
14
17
  export function installRateLimiter(input, deps) {
15
- const providers = input.provider;
16
- deps.logger.trace("ratelimit_install_start", {
17
- providerCount: providers ? Object.keys(providers).length : 0
18
- });
19
- if (!providers) {
20
- deps.logger.info("ratelimit_plugin_initialized", { providerCount: 0 });
21
- return;
22
- }
23
- let enabledCount = 0;
24
- for (const [providerId, providerConfig] of Object.entries(providers)) {
25
- if (!providerConfig) {
26
- continue;
27
- }
28
- deps.logger.trace("ratelimit_provider_optin_check", {
29
- providerId,
30
- hasOptions: Boolean(providerConfig.options)
31
- });
32
- const opts = parseRateLimitOptions(providerConfig.options);
33
- if (!opts) {
34
- deps.logger.debug("ratelimit_provider_skipped", { providerId, reason: "not_opted_in" });
35
- continue;
36
- }
37
- deps.logger.trace("ratelimit_provider_optin_resolved", {
38
- providerId,
39
- scope: opts.scope,
40
- headerPrefix: opts.headerPrefix,
41
- tiers: opts.tiers.length
42
- });
43
- const options = (providerConfig.options ??= {});
44
- // Compose with any fetch a prior plugin already installed (capture it now,
45
- // not at call time, so we delegate to the original not to ourselves).
46
- const delegate = typeof options.fetch === "function" ? options.fetch : undefined;
47
- deps.logger.trace("ratelimit_fetch_wrapped", {
48
- providerId,
49
- composedWithExistingFetch: Boolean(delegate)
50
- });
51
- const store = new Map();
52
- options.fetch = makeRateLimitFetch(providerId, opts, store, deps, delegate);
53
- enabledCount += 1;
54
- deps.logger.info("ratelimit_provider_enabled", {
55
- providerId,
56
- scope: opts.scope,
57
- tiers: opts.tiers.length,
58
- headerPrefix: opts.headerPrefix
59
- });
60
- }
61
- deps.logger.info("ratelimit_plugin_initialized", { providerCount: enabledCount });
18
+ const providers = input.provider;
19
+ deps.logger.trace("ratelimit_install_start", { providerCount: providers ? Object.keys(providers).length : 0 });
20
+ if (!providers) {
21
+ deps.logger.info("ratelimit_plugin_initialized", { providerCount: 0 });
22
+ return;
23
+ }
24
+ let enabledCount = 0;
25
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
26
+ if (!providerConfig) {
27
+ continue;
28
+ }
29
+ deps.logger.trace("ratelimit_provider_optin_check", {
30
+ providerId,
31
+ hasOptions: Boolean(providerConfig.options)
32
+ });
33
+ const opts = parseRateLimitOptions(providerConfig.options);
34
+ if (!opts) {
35
+ deps.logger.debug("ratelimit_provider_skipped", {
36
+ providerId,
37
+ reason: "not_opted_in"
38
+ });
39
+ continue;
40
+ }
41
+ deps.logger.trace("ratelimit_provider_optin_resolved", {
42
+ providerId,
43
+ scope: opts.scope,
44
+ headerPrefix: opts.headerPrefix,
45
+ tiers: opts.tiers.length
46
+ });
47
+ const options = providerConfig.options ??= {};
48
+ // Compose with any fetch a prior plugin already installed (capture it now,
49
+ // not at call time, so we delegate to the original not to ourselves).
50
+ const delegate = typeof options.fetch === "function" ? options.fetch : undefined;
51
+ deps.logger.trace("ratelimit_fetch_wrapped", {
52
+ providerId,
53
+ composedWithExistingFetch: Boolean(delegate)
54
+ });
55
+ const store = new Map();
56
+ options.fetch = makeRateLimitFetch(providerId, opts, store, deps, delegate);
57
+ enabledCount += 1;
58
+ deps.logger.info("ratelimit_provider_enabled", {
59
+ providerId,
60
+ scope: opts.scope,
61
+ tiers: opts.tiers.length,
62
+ headerPrefix: opts.headerPrefix
63
+ });
64
+ }
65
+ deps.logger.info("ratelimit_plugin_initialized", { providerCount: enabledCount });
62
66
  }
63
67
  /**
64
- * Build the fetch wrapper for one provider. The wrapper:
65
- * 1. Resolves the bucket (provider, or `(provider, model)` under `scope: "model"`).
66
- * 2. Pre-request gate: if that bucket's cooldown is armed, waits until it clears.
67
- * 3. Sends the request via the underlying fetch.
68
- * 4. Reads the rate-limit headers; selects the policy tier by reset magnitude.
69
- * On `remaining: 0` it arms the gate for the next callers (only when the
70
- * matched tier is `"wait"`).
71
- * 5. On a 429: a `"wait"` tier waits the reset window and retries (up to the
72
- * tier's `maxRetries`); an `"error"` tier surfaces the 429 immediately.
73
- *
74
- * The Response is returned untouched (no `.clone()`) — we only read `status`
75
- * and `headers`, so the body stream is delivered to OpenCode intact.
76
- */
68
+ * Build the fetch wrapper for one provider. The wrapper:
69
+ * 1. Resolves the bucket (provider, or `(provider, model)` under `scope: "model"`).
70
+ * 2. Pre-request gate: if that bucket's cooldown is armed, waits until it clears.
71
+ * 3. Sends the request via the underlying fetch.
72
+ * 4. Reads the rate-limit headers; selects the policy tier by reset magnitude.
73
+ * On `remaining: 0` it arms the gate for the next callers (only when the
74
+ * matched tier is `"wait"`).
75
+ * 5. On a 429: a `"wait"` tier waits the reset window and retries (up to the
76
+ * tier's `maxRetries`); an `"error"` tier surfaces the 429 immediately.
77
+ *
78
+ * The Response is returned untouched (no `.clone()`) — we only read `status`
79
+ * and `headers`, so the body stream is delivered to OpenCode intact.
80
+ */
77
81
  export function makeRateLimitFetch(providerId, opts, store, deps, delegate) {
78
- const now = deps.now ?? Date.now;
79
- const sleep = deps.sleep ?? defaultSleep;
80
- const underlying = delegate ?? deps.fetchImpl ?? globalThis.fetch;
81
- const { logger } = deps;
82
- const isRequest = (value) => typeof Request !== "undefined" && value instanceof Request;
83
- const wrapped = async (input, init) => {
84
- // Honor an abort signal whether it rides on `init` or on a `Request` input.
85
- const signal = init?.signal ?? (isRequest(input) ? input.signal : undefined) ?? undefined;
86
- const model = opts.scope === "model" ? await modelFromRequest(input, init) : undefined;
87
- if (opts.scope === "model") {
88
- logger.trace("ratelimit_model_resolved", { providerId, model, matched: model !== undefined });
89
- }
90
- const key = model ? `${providerId}\u0000${model}` : providerId;
91
- let state = store.get(key);
92
- const bucketExisted = state !== undefined;
93
- if (!state) {
94
- state = createProviderState();
95
- store.set(key, state);
96
- }
97
- logger.trace("ratelimit_fetch_invoked", {
98
- providerId,
99
- scope: opts.scope,
100
- model,
101
- bucketKey: key,
102
- bucketExisted,
103
- cooldownUntilMs: state.cooldownUntilMs
104
- });
105
- // 2. Pre-request gate — wait out a cooldown a previous "wait" tier armed.
106
- if (state.cooldownUntilMs > now()) {
107
- const waitMs = clampWait(state.cooldownUntilMs - now(), state.cooldownMaxWaitMs);
108
- logger.trace("ratelimit_cooldown_gate_armed", {
109
- providerId,
110
- model,
111
- cooldownUntilMs: state.cooldownUntilMs,
112
- remainingMs: state.cooldownUntilMs - now(),
113
- clampedWaitMs: waitMs,
114
- maxWaitMs: state.cooldownMaxWaitMs
115
- });
116
- if (waitMs > 0) {
117
- logger.info("ratelimit_throttle_wait", { providerId, model, waitMs });
118
- await waitGate(state, sleep, now, state.cooldownMaxWaitMs, signal, logger, providerId, model, "pre_request");
119
- }
120
- }
121
- // 3-5. Attempt loop.
122
- let attempt = 0;
123
- for (;;) {
124
- // A Request body is single-use; send a fresh clone each attempt so a
125
- // wait-tier retry doesn't fail with "body already used". (The original is
126
- // never sent directly, so each clone has an unconsumed body.)
127
- const attemptInput = isRequest(input) ? input.clone() : input;
128
- logger.trace("ratelimit_underlying_fetch", { providerId, model, attempt });
129
- const response = await underlying(attemptInput, init);
130
- const snapshot = readSnapshot(response, opts, now(), logger, providerId);
131
- logger.trace("ratelimit_snapshot_parsed", {
132
- providerId,
133
- model,
134
- status: response.status,
135
- limit: snapshot.limit,
136
- remaining: snapshot.remaining,
137
- resetSeconds: snapshot.resetSeconds,
138
- resetAtMs: snapshot.resetAtMs,
139
- retryAfterMs: snapshot.retryAfterMs
140
- });
141
- logger.debug("ratelimit_quota", {
142
- providerId,
143
- model,
144
- remaining: snapshot.remaining,
145
- limit: snapshot.limit,
146
- resetSeconds: snapshot.resetSeconds
147
- });
148
- if (response.status !== 429) {
149
- // Arm the gate for the NEXT callers when the window is exhausted — but
150
- // only under a "wait" tier. An "error" tier wants the next request to
151
- // hit a real 429 and be surfaced, so we leave the gate unarmed.
152
- if (snapshot.remaining !== undefined &&
153
- snapshot.remaining <= 0 &&
154
- snapshot.resetAtMs !== undefined) {
155
- const resetForTier = effectiveResetSeconds(snapshot);
156
- const tier = selectTier(opts.tiers, resetForTier);
157
- logger.trace("ratelimit_exhausted_tier_selected", {
158
- providerId,
159
- model,
160
- resetSeconds: resetForTier,
161
- maxResetSeconds: tier.maxResetSeconds,
162
- action: tier.action,
163
- maxWaitMs: tier.maxWaitMs,
164
- maxRetries: tier.maxRetries
165
- });
166
- if (tier.action === "wait") {
167
- state.cooldownUntilMs = snapshot.resetAtMs;
168
- state.cooldownMaxWaitMs = tier.maxWaitMs;
169
- logger.trace("ratelimit_cooldown_set", {
170
- providerId,
171
- model,
172
- cooldownUntilMs: state.cooldownUntilMs,
173
- maxWaitMs: tier.maxWaitMs,
174
- source: "exhausted"
175
- });
176
- }
177
- else {
178
- // Error tier → don't arm, and drop any stale cooldown a prior
179
- // "wait" window left so the next request hits a real 429 at once.
180
- state.cooldownUntilMs = 0;
181
- state.cooldownMaxWaitMs = 0;
182
- logger.trace("ratelimit_cooldown_cleared", {
183
- providerId,
184
- model,
185
- source: "exhausted_error_tier"
186
- });
187
- }
188
- }
189
- return response;
190
- }
191
- const resetForTier = effectiveResetSeconds(snapshot);
192
- const tier = selectTier(opts.tiers, resetForTier);
193
- logger.trace("ratelimit_429_tier_selected", {
194
- providerId,
195
- model,
196
- attempt,
197
- resetSeconds: resetForTier,
198
- maxResetSeconds: tier.maxResetSeconds,
199
- action: tier.action,
200
- maxWaitMs: tier.maxWaitMs,
201
- maxRetries: tier.maxRetries
202
- });
203
- if (tier.action === "error") {
204
- // Drop any cooldown a prior "wait" tier armed, so the fail-fast is
205
- // actually fast — later requests must not sleep a stale window first.
206
- state.cooldownUntilMs = 0;
207
- state.cooldownMaxWaitMs = 0;
208
- logger.trace("ratelimit_cooldown_cleared", {
209
- providerId,
210
- model,
211
- source: "429_error_tier"
212
- });
213
- logger.warn("ratelimit_failfast", {
214
- providerId,
215
- model,
216
- resetSeconds: snapshot.resetSeconds
217
- });
218
- return response;
219
- }
220
- if (attempt >= tier.maxRetries) {
221
- logger.trace("ratelimit_retries_exhausted", {
222
- providerId,
223
- model,
224
- attempt,
225
- maxRetries: tier.maxRetries
226
- });
227
- logger.error("ratelimit_giveup", { providerId, model, attempts: attempt });
228
- return response;
229
- }
230
- const backoffMs = computeBackoff(snapshot, now());
231
- const waitMs = clampWait(backoffMs, tier.maxWaitMs);
232
- logger.trace("ratelimit_429_backoff_computed", {
233
- providerId,
234
- model,
235
- attempt: attempt + 1,
236
- rawBackoffMs: backoffMs,
237
- clampedWaitMs: waitMs,
238
- maxWaitMs: tier.maxWaitMs
239
- });
240
- state.cooldownUntilMs = now() + waitMs;
241
- state.cooldownMaxWaitMs = tier.maxWaitMs;
242
- logger.trace("ratelimit_cooldown_set", {
243
- providerId,
244
- model,
245
- cooldownUntilMs: state.cooldownUntilMs,
246
- maxWaitMs: tier.maxWaitMs,
247
- source: "429_backoff"
248
- });
249
- logger.warn("ratelimit_429_backoff", {
250
- providerId,
251
- model,
252
- attempt: attempt + 1,
253
- waitMs,
254
- resetSeconds: snapshot.resetSeconds
255
- });
256
- await waitGate(state, sleep, now, tier.maxWaitMs, signal, logger, providerId, model, "backoff");
257
- attempt += 1;
258
- }
259
- };
260
- return wrapped;
82
+ const now = deps.now ?? Date.now;
83
+ const sleep = deps.sleep ?? defaultSleep;
84
+ const underlying = delegate ?? deps.fetchImpl ?? globalThis.fetch;
85
+ const { logger } = deps;
86
+ const isRequest = (value) => typeof Request !== "undefined" && value instanceof Request;
87
+ const wrapped = async (input, init) => {
88
+ // Honor an abort signal whether it rides on `init` or on a `Request` input.
89
+ const signal = init?.signal ?? (isRequest(input) ? input.signal : undefined) ?? undefined;
90
+ const model = opts.scope === "model" ? await modelFromRequest(input, init) : undefined;
91
+ if (opts.scope === "model") {
92
+ logger.trace("ratelimit_model_resolved", {
93
+ providerId,
94
+ model,
95
+ matched: model !== undefined
96
+ });
97
+ }
98
+ const key = model ? `${providerId}\u0000${model}` : providerId;
99
+ let state = store.get(key);
100
+ const bucketExisted = state !== undefined;
101
+ if (!state) {
102
+ state = createProviderState();
103
+ store.set(key, state);
104
+ }
105
+ logger.trace("ratelimit_fetch_invoked", {
106
+ providerId,
107
+ scope: opts.scope,
108
+ model,
109
+ bucketKey: key,
110
+ bucketExisted,
111
+ cooldownUntilMs: state.cooldownUntilMs
112
+ });
113
+ // 2. Pre-request gate — wait out a cooldown a previous "wait" tier armed.
114
+ if (state.cooldownUntilMs > now()) {
115
+ const waitMs = clampWait(state.cooldownUntilMs - now(), state.cooldownMaxWaitMs);
116
+ logger.trace("ratelimit_cooldown_gate_armed", {
117
+ providerId,
118
+ model,
119
+ cooldownUntilMs: state.cooldownUntilMs,
120
+ remainingMs: state.cooldownUntilMs - now(),
121
+ clampedWaitMs: waitMs,
122
+ maxWaitMs: state.cooldownMaxWaitMs
123
+ });
124
+ if (waitMs > 0) {
125
+ logger.info("ratelimit_throttle_wait", {
126
+ providerId,
127
+ model,
128
+ waitMs
129
+ });
130
+ await waitGate(state, sleep, now, state.cooldownMaxWaitMs, signal, logger, providerId, model, "pre_request");
131
+ }
132
+ }
133
+ // 3-5. Attempt loop.
134
+ let attempt = 0;
135
+ for (;;) {
136
+ // A Request body is single-use; send a fresh clone each attempt so a
137
+ // wait-tier retry doesn't fail with "body already used". (The original is
138
+ // never sent directly, so each clone has an unconsumed body.)
139
+ const attemptInput = isRequest(input) ? input.clone() : input;
140
+ logger.trace("ratelimit_underlying_fetch", {
141
+ providerId,
142
+ model,
143
+ attempt
144
+ });
145
+ const response = await underlying(attemptInput, init);
146
+ const snapshot = readSnapshot(response, opts, now(), logger, providerId);
147
+ logger.trace("ratelimit_snapshot_parsed", {
148
+ providerId,
149
+ model,
150
+ status: response.status,
151
+ limit: snapshot.limit,
152
+ remaining: snapshot.remaining,
153
+ resetSeconds: snapshot.resetSeconds,
154
+ resetAtMs: snapshot.resetAtMs,
155
+ retryAfterMs: snapshot.retryAfterMs
156
+ });
157
+ logger.debug("ratelimit_quota", {
158
+ providerId,
159
+ model,
160
+ remaining: snapshot.remaining,
161
+ limit: snapshot.limit,
162
+ resetSeconds: snapshot.resetSeconds
163
+ });
164
+ if (response.status !== 429) {
165
+ // Arm the gate for the NEXT callers when the window is exhausted — but
166
+ // only under a "wait" tier. An "error" tier wants the next request to
167
+ // hit a real 429 and be surfaced, so we leave the gate unarmed.
168
+ if (snapshot.remaining !== undefined && snapshot.remaining <= 0 && snapshot.resetAtMs !== undefined) {
169
+ const resetForTier = effectiveResetSeconds(snapshot);
170
+ const tier = selectTier(opts.tiers, resetForTier);
171
+ logger.trace("ratelimit_exhausted_tier_selected", {
172
+ providerId,
173
+ model,
174
+ resetSeconds: resetForTier,
175
+ maxResetSeconds: tier.maxResetSeconds,
176
+ action: tier.action,
177
+ maxWaitMs: tier.maxWaitMs,
178
+ maxRetries: tier.maxRetries
179
+ });
180
+ if (tier.action === "wait") {
181
+ state.cooldownUntilMs = snapshot.resetAtMs;
182
+ state.cooldownMaxWaitMs = tier.maxWaitMs;
183
+ logger.trace("ratelimit_cooldown_set", {
184
+ providerId,
185
+ model,
186
+ cooldownUntilMs: state.cooldownUntilMs,
187
+ maxWaitMs: tier.maxWaitMs,
188
+ source: "exhausted"
189
+ });
190
+ } else {
191
+ // Error tier → don't arm, and drop any stale cooldown a prior
192
+ // "wait" window left so the next request hits a real 429 at once.
193
+ state.cooldownUntilMs = 0;
194
+ state.cooldownMaxWaitMs = 0;
195
+ logger.trace("ratelimit_cooldown_cleared", {
196
+ providerId,
197
+ model,
198
+ source: "exhausted_error_tier"
199
+ });
200
+ }
201
+ }
202
+ return response;
203
+ }
204
+ const resetForTier = effectiveResetSeconds(snapshot);
205
+ const tier = selectTier(opts.tiers, resetForTier);
206
+ logger.trace("ratelimit_429_tier_selected", {
207
+ providerId,
208
+ model,
209
+ attempt,
210
+ resetSeconds: resetForTier,
211
+ maxResetSeconds: tier.maxResetSeconds,
212
+ action: tier.action,
213
+ maxWaitMs: tier.maxWaitMs,
214
+ maxRetries: tier.maxRetries
215
+ });
216
+ if (tier.action === "error") {
217
+ // Drop any cooldown a prior "wait" tier armed, so the fail-fast is
218
+ // actually fast — later requests must not sleep a stale window first.
219
+ state.cooldownUntilMs = 0;
220
+ state.cooldownMaxWaitMs = 0;
221
+ logger.trace("ratelimit_cooldown_cleared", {
222
+ providerId,
223
+ model,
224
+ source: "429_error_tier"
225
+ });
226
+ logger.warn("ratelimit_failfast", {
227
+ providerId,
228
+ model,
229
+ resetSeconds: snapshot.resetSeconds
230
+ });
231
+ return response;
232
+ }
233
+ if (attempt >= tier.maxRetries) {
234
+ logger.trace("ratelimit_retries_exhausted", {
235
+ providerId,
236
+ model,
237
+ attempt,
238
+ maxRetries: tier.maxRetries
239
+ });
240
+ logger.error("ratelimit_giveup", {
241
+ providerId,
242
+ model,
243
+ attempts: attempt
244
+ });
245
+ return response;
246
+ }
247
+ const backoffMs = computeBackoff(snapshot, now());
248
+ const waitMs = clampWait(backoffMs, tier.maxWaitMs);
249
+ logger.trace("ratelimit_429_backoff_computed", {
250
+ providerId,
251
+ model,
252
+ attempt: attempt + 1,
253
+ rawBackoffMs: backoffMs,
254
+ clampedWaitMs: waitMs,
255
+ maxWaitMs: tier.maxWaitMs
256
+ });
257
+ state.cooldownUntilMs = now() + waitMs;
258
+ state.cooldownMaxWaitMs = tier.maxWaitMs;
259
+ logger.trace("ratelimit_cooldown_set", {
260
+ providerId,
261
+ model,
262
+ cooldownUntilMs: state.cooldownUntilMs,
263
+ maxWaitMs: tier.maxWaitMs,
264
+ source: "429_backoff"
265
+ });
266
+ logger.warn("ratelimit_429_backoff", {
267
+ providerId,
268
+ model,
269
+ attempt: attempt + 1,
270
+ waitMs,
271
+ resetSeconds: snapshot.resetSeconds
272
+ });
273
+ await waitGate(state, sleep, now, tier.maxWaitMs, signal, logger, providerId, model, "backoff");
274
+ attempt += 1;
275
+ }
276
+ };
277
+ return wrapped;
261
278
  }
262
279
  /**
263
- * Best-effort extraction of the `model` from an OpenAI-compatible request.
264
- * The AI SDK calls `fetch(url, { body: "<json>" })`, so the common path reads a
265
- * JSON string body synchronously (non-destructive). It also supports the
266
- * `fetch(new Request(...))` shape by reading a clone of the Request body, so a
267
- * Request-style caller doesn't silently collapse to the provider-wide bucket.
268
- * Anything unparseable → `undefined` → caller falls back to the provider bucket.
269
- */
280
+ * Best-effort extraction of the `model` from an OpenAI-compatible request.
281
+ * The AI SDK calls `fetch(url, { body: "<json>" })`, so the common path reads a
282
+ * JSON string body synchronously (non-destructive). It also supports the
283
+ * `fetch(new Request(...))` shape by reading a clone of the Request body, so a
284
+ * Request-style caller doesn't silently collapse to the provider-wide bucket.
285
+ * Anything unparseable → `undefined` → caller falls back to the provider bucket.
286
+ */
270
287
  async function modelFromRequest(input, init) {
271
- const fromInit = modelFromBody(init?.body);
272
- if (fromInit !== undefined) {
273
- return fromInit;
274
- }
275
- if (typeof Request !== "undefined" && input instanceof Request) {
276
- try {
277
- return modelFromBody(await input.clone().text());
278
- }
279
- catch {
280
- return undefined;
281
- }
282
- }
283
- return undefined;
288
+ const fromInit = modelFromBody(init?.body);
289
+ if (fromInit !== undefined) {
290
+ return fromInit;
291
+ }
292
+ if (typeof Request !== "undefined" && input instanceof Request) {
293
+ try {
294
+ return modelFromBody(await input.clone().text());
295
+ } catch {
296
+ return undefined;
297
+ }
298
+ }
299
+ return undefined;
284
300
  }
285
301
  function modelFromBody(body) {
286
- if (typeof body !== "string") {
287
- return undefined;
288
- }
289
- try {
290
- const parsed = JSON.parse(body);
291
- return typeof parsed.model === "string" && parsed.model.length > 0 ? parsed.model : undefined;
292
- }
293
- catch {
294
- return undefined;
295
- }
302
+ if (typeof body !== "string") {
303
+ return undefined;
304
+ }
305
+ try {
306
+ const parsed = JSON.parse(body);
307
+ return typeof parsed.model === "string" && parsed.model.length > 0 ? parsed.model : undefined;
308
+ } catch {
309
+ return undefined;
310
+ }
296
311
  }
297
312
  /**
298
- * Reset magnitude (seconds) used for tier selection: prefer `x-ratelimit-reset`,
299
- * else derive from a `Retry-After` fallback — so a `Retry-After`-only 429 with a
300
- * multi-day delay still lands in a long-reset (`error`) tier instead of the
301
- * smallest band.
302
- */
313
+ * Reset magnitude (seconds) used for tier selection: prefer `x-ratelimit-reset`,
314
+ * else derive from a `Retry-After` fallback — so a `Retry-After`-only 429 with a
315
+ * multi-day delay still lands in a long-reset (`error`) tier instead of the
316
+ * smallest band.
317
+ */
303
318
  function effectiveResetSeconds(snapshot) {
304
- if (snapshot.resetSeconds !== undefined) {
305
- return snapshot.resetSeconds;
306
- }
307
- if (snapshot.retryAfterMs !== undefined) {
308
- return Math.ceil(snapshot.retryAfterMs / 1000);
309
- }
310
- return undefined;
319
+ if (snapshot.resetSeconds !== undefined) {
320
+ return snapshot.resetSeconds;
321
+ }
322
+ if (snapshot.retryAfterMs !== undefined) {
323
+ return Math.ceil(snapshot.retryAfterMs / 1e3);
324
+ }
325
+ return undefined;
311
326
  }
312
327
  function readSnapshot(response, opts, nowMs, logger, providerId) {
313
- try {
314
- return parseRateLimit(response.headers, opts.headerPrefix, nowMs);
315
- }
316
- catch (error) {
317
- logger.debug("ratelimit_header_parse_failed", {
318
- providerId,
319
- error: error instanceof Error ? error.message : String(error)
320
- });
321
- return {};
322
- }
328
+ try {
329
+ return parseRateLimit(response.headers, opts.headerPrefix, nowMs);
330
+ } catch (error) {
331
+ logger.debug("ratelimit_header_parse_failed", {
332
+ providerId,
333
+ error: error instanceof Error ? error.message : String(error)
334
+ });
335
+ return {};
336
+ }
323
337
  }
324
338
  function computeBackoff(snapshot, nowMs) {
325
- if (snapshot.resetAtMs !== undefined) {
326
- return Math.max(0, snapshot.resetAtMs - nowMs);
327
- }
328
- if (snapshot.retryAfterMs !== undefined) {
329
- return snapshot.retryAfterMs;
330
- }
331
- return DEFAULT_BACKOFF_MS;
339
+ if (snapshot.resetAtMs !== undefined) {
340
+ return Math.max(0, snapshot.resetAtMs - nowMs);
341
+ }
342
+ if (snapshot.retryAfterMs !== undefined) {
343
+ return snapshot.retryAfterMs;
344
+ }
345
+ return DEFAULT_BACKOFF_MS;
332
346
  }
333
347
  /** `maxWaitMs` of 0 means unlimited (wait the full reset window). */
334
348
  function clampWait(ms, maxWaitMs) {
335
- const nonNegative = Math.max(0, ms);
336
- return maxWaitMs > 0 ? Math.min(nonNegative, maxWaitMs) : nonNegative;
349
+ const nonNegative = Math.max(0, ms);
350
+ return maxWaitMs > 0 ? Math.min(nonNegative, maxWaitMs) : nonNegative;
337
351
  }
338
352
  /**
339
- * Wait until `state.cooldownUntilMs` has elapsed. Two concurrency properties:
340
- *
341
- * - **One shared timer.** The first caller to hit the gate creates the timer on
342
- * `state.cooldownPromise`; concurrent callers await that same timer rather
343
- * than each starting their own, so a burst during cooldown produces ONE wait,
344
- * not N. The shared timer is not tied to any single caller's `signal` — each
345
- * caller races it against its own signal (see `raceWithAbort`), so one
346
- * request's cancellation never aborts the others.
347
- * - **Honors the longest window.** After the shared timer resolves we re-check
348
- * `cooldownUntilMs`. If another caller extended the window (e.g. a 429 landed
349
- * with a further-out reset) while we were waiting we wait again for the
350
- * remainder instead of returning early and hammering the gateway.
351
- *
352
- * `maxWaitMs` (when > 0) bounds the TOTAL wait of a single call: once a caller
353
- * has waited that long it proceeds even if the window hasn't fully elapsed. A
354
- * caller that needs LESS than the in-flight shared timer (the window shrank, or
355
- * the timer was created by a caller with a later deadline) falls back to its own
356
- * private sleep so it is never held past its own required time.
357
- */
353
+ * Wait until `state.cooldownUntilMs` has elapsed. Two concurrency properties:
354
+ *
355
+ * - **One shared timer.** The first caller to hit the gate creates the timer on
356
+ * `state.cooldownPromise`; concurrent callers await that same timer rather
357
+ * than each starting their own, so a burst during cooldown produces ONE wait,
358
+ * not N. The shared timer is not tied to any single caller's `signal` — each
359
+ * caller races it against its own signal (see `raceWithAbort`), so one
360
+ * request's cancellation never aborts the others.
361
+ * - **Honors the longest window.** After the shared timer resolves we re-check
362
+ * `cooldownUntilMs`. If another caller extended the window (e.g. a 429 landed
363
+ * with a further-out reset) while we were waiting we wait again for the
364
+ * remainder instead of returning early and hammering the gateway.
365
+ *
366
+ * `maxWaitMs` (when > 0) bounds the TOTAL wait of a single call: once a caller
367
+ * has waited that long it proceeds even if the window hasn't fully elapsed. A
368
+ * caller that needs LESS than the in-flight shared timer (the window shrank, or
369
+ * the timer was created by a caller with a later deadline) falls back to its own
370
+ * private sleep so it is never held past its own required time.
371
+ */
358
372
  async function waitGate(state, sleep, now, maxWaitMs, signal, logger, providerId, model, phase) {
359
- const deadlineMs = maxWaitMs > 0 ? now() + maxWaitMs : Number.POSITIVE_INFINITY;
360
- logger.trace("ratelimit_wait_start", {
361
- providerId,
362
- model,
363
- phase,
364
- cooldownUntilMs: state.cooldownUntilMs,
365
- maxWaitMs
366
- });
367
- for (;;) {
368
- const target = Math.min(state.cooldownUntilMs, deadlineMs);
369
- const remainingMs = target - now();
370
- if (remainingMs <= 0) {
371
- logger.trace("ratelimit_wait_end", { providerId, model, phase });
372
- return;
373
- }
374
- logger.trace("ratelimit_wait_tick", {
375
- providerId,
376
- model,
377
- phase,
378
- remainingMs,
379
- sharedTimer: state.cooldownPromise !== undefined
380
- });
381
- // Reuse the shared timer only if it won't make us wait longer than we need.
382
- let pending = state.cooldownPromise;
383
- if (pending &&
384
- state.cooldownPromiseUntilMs !== undefined &&
385
- state.cooldownPromiseUntilMs - now() > remainingMs) {
386
- pending = undefined;
387
- }
388
- if (!pending) {
389
- const created = sleep(remainingMs).finally(() => {
390
- if (state.cooldownPromise === created) {
391
- state.cooldownPromise = undefined;
392
- state.cooldownPromiseUntilMs = undefined;
393
- }
394
- });
395
- // Publish as the shared timer only when there isn't already a (shorter)
396
- // one in flight — never clobber it with our longer/private wait.
397
- if (!state.cooldownPromise) {
398
- state.cooldownPromise = created;
399
- state.cooldownPromiseUntilMs = now() + remainingMs;
400
- }
401
- pending = created;
402
- }
403
- try {
404
- await raceWithAbort(pending, signal);
405
- }
406
- catch (error) {
407
- if (isAbortError(error)) {
408
- logger.warn("ratelimit_wait_aborted", { providerId, model, phase });
409
- }
410
- throw error;
411
- }
412
- // Loop: re-check in case the window was extended while we waited.
413
- }
373
+ const deadlineMs = maxWaitMs > 0 ? now() + maxWaitMs : Number.POSITIVE_INFINITY;
374
+ logger.trace("ratelimit_wait_start", {
375
+ providerId,
376
+ model,
377
+ phase,
378
+ cooldownUntilMs: state.cooldownUntilMs,
379
+ maxWaitMs
380
+ });
381
+ for (;;) {
382
+ const target = Math.min(state.cooldownUntilMs, deadlineMs);
383
+ const remainingMs = target - now();
384
+ if (remainingMs <= 0) {
385
+ logger.trace("ratelimit_wait_end", {
386
+ providerId,
387
+ model,
388
+ phase
389
+ });
390
+ return;
391
+ }
392
+ logger.trace("ratelimit_wait_tick", {
393
+ providerId,
394
+ model,
395
+ phase,
396
+ remainingMs,
397
+ sharedTimer: state.cooldownPromise !== undefined
398
+ });
399
+ // Reuse the shared timer only if it won't make us wait longer than we need.
400
+ let pending = state.cooldownPromise;
401
+ if (pending && state.cooldownPromiseUntilMs !== undefined && state.cooldownPromiseUntilMs - now() > remainingMs) {
402
+ pending = undefined;
403
+ }
404
+ if (!pending) {
405
+ const created = sleep(remainingMs).finally(() => {
406
+ if (state.cooldownPromise === created) {
407
+ state.cooldownPromise = undefined;
408
+ state.cooldownPromiseUntilMs = undefined;
409
+ }
410
+ });
411
+ // Publish as the shared timer only when there isn't already a (shorter)
412
+ // one in flight — never clobber it with our longer/private wait.
413
+ if (!state.cooldownPromise) {
414
+ state.cooldownPromise = created;
415
+ state.cooldownPromiseUntilMs = now() + remainingMs;
416
+ }
417
+ pending = created;
418
+ }
419
+ try {
420
+ await raceWithAbort(pending, signal);
421
+ } catch (error) {
422
+ if (isAbortError(error)) {
423
+ logger.warn("ratelimit_wait_aborted", {
424
+ providerId,
425
+ model,
426
+ phase
427
+ });
428
+ }
429
+ throw error;
430
+ }
431
+ }
414
432
  }
415
433
  function raceWithAbort(promise, signal) {
416
- if (!signal) {
417
- return promise;
418
- }
419
- if (signal.aborted) {
420
- return Promise.reject(toAbortError(signal));
421
- }
422
- return new Promise((resolve, reject) => {
423
- const onAbort = () => reject(toAbortError(signal));
424
- signal.addEventListener("abort", onAbort, { once: true });
425
- promise.then(() => {
426
- signal.removeEventListener("abort", onAbort);
427
- resolve();
428
- }, (error) => {
429
- signal.removeEventListener("abort", onAbort);
430
- reject(error);
431
- });
432
- });
434
+ if (!signal) {
435
+ return promise;
436
+ }
437
+ if (signal.aborted) {
438
+ return Promise.reject(toAbortError(signal));
439
+ }
440
+ return new Promise((resolve, reject) => {
441
+ const onAbort = () => reject(toAbortError(signal));
442
+ signal.addEventListener("abort", onAbort, { once: true });
443
+ promise.then(() => {
444
+ signal.removeEventListener("abort", onAbort);
445
+ resolve();
446
+ }, (error) => {
447
+ signal.removeEventListener("abort", onAbort);
448
+ reject(error);
449
+ });
450
+ });
433
451
  }
434
452
  function defaultSleep(ms, signal) {
435
- return new Promise((resolve, reject) => {
436
- if (signal?.aborted) {
437
- reject(toAbortError(signal));
438
- return;
439
- }
440
- const timer = setTimeout(() => {
441
- signal?.removeEventListener("abort", onAbort);
442
- resolve();
443
- }, ms);
444
- const onAbort = () => {
445
- clearTimeout(timer);
446
- reject(toAbortError(signal));
447
- };
448
- signal?.addEventListener("abort", onAbort, { once: true });
449
- });
453
+ return new Promise((resolve, reject) => {
454
+ if (signal?.aborted) {
455
+ reject(toAbortError(signal));
456
+ return;
457
+ }
458
+ const timer = setTimeout(() => {
459
+ signal?.removeEventListener("abort", onAbort);
460
+ resolve();
461
+ }, ms);
462
+ const onAbort = () => {
463
+ clearTimeout(timer);
464
+ reject(toAbortError(signal));
465
+ };
466
+ signal?.addEventListener("abort", onAbort, { once: true });
467
+ });
450
468
  }
451
469
  function toAbortError(signal) {
452
- const reason = signal.reason;
453
- if (reason instanceof Error) {
454
- return reason;
455
- }
456
- return new DOMException("The operation was aborted", "AbortError");
470
+ const reason = signal.reason;
471
+ if (reason instanceof Error) {
472
+ return reason;
473
+ }
474
+ return new DOMException("The operation was aborted", "AbortError");
457
475
  }
458
476
  function isAbortError(error) {
459
- return error instanceof Error && error.name === "AbortError";
477
+ return error instanceof Error && error.name === "AbortError";
460
478
  }
479
+
461
480
  //# sourceMappingURL=plugin.js.map