@hackerrank/astra-cli 0.1.24 → 0.1.25
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/package.json +1 -1
- package/src/model.js +178 -11
package/package.json
CHANGED
package/src/model.js
CHANGED
|
@@ -42,7 +42,7 @@ export class GatewayModel {
|
|
|
42
42
|
* @param {number} [opts.maxRetries]
|
|
43
43
|
* @param {(info:object)=>void} [opts.onRetry] called before each retry sleep
|
|
44
44
|
*/
|
|
45
|
-
constructor({ model, baseUrl, apiKey, modelKwargs = {}, maxRetries =
|
|
45
|
+
constructor({ model, baseUrl, apiKey, modelKwargs = {}, maxRetries = 8, maxTokens = 8192, requestTimeoutMs = 0, onRetry } = {}) {
|
|
46
46
|
if (!model) throw new Error("GatewayModel: `model` is required");
|
|
47
47
|
this.model = model;
|
|
48
48
|
this.maxTokens = maxTokens;
|
|
@@ -124,14 +124,15 @@ export class GatewayModel {
|
|
|
124
124
|
// Retry on transient server / rate-limit errors with exponential
|
|
125
125
|
// backoff (honoring Retry-After when the server provides it).
|
|
126
126
|
if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {
|
|
127
|
-
const
|
|
127
|
+
const serverWait = retryAfterMs(res.headers, text);
|
|
128
|
+
const wait = serverWait != null ? Math.max(serverWait + 250, 1000) : backoffMs(attempt);
|
|
128
129
|
this.nRetries++;
|
|
129
130
|
this.onRetry({
|
|
130
131
|
attempt: attempt + 1,
|
|
131
132
|
maxRetries: this.maxRetries,
|
|
132
133
|
status: res.status,
|
|
133
134
|
waitMs: wait,
|
|
134
|
-
reason: `HTTP ${res.status}`,
|
|
135
|
+
reason: res.status === 429 ? "HTTP 429 (rate limited)" : `HTTP ${res.status}`,
|
|
135
136
|
});
|
|
136
137
|
await sleep(wait);
|
|
137
138
|
continue;
|
|
@@ -282,14 +283,180 @@ function hintForStatus(status) {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Parse a duration string, number, or date into milliseconds.
|
|
288
|
+
* Supports numbers (seconds or timestamps), units (ms, s, m, h, d),
|
|
289
|
+
* composite durations ("1m 30s"), and HTTP/ISO dates.
|
|
290
|
+
*/
|
|
291
|
+
export function parseDurationMs(val) {
|
|
292
|
+
if (typeof val === "number" && Number.isFinite(val) && val >= 0) {
|
|
293
|
+
if (val > 1e11) return Math.max(0, val - Date.now());
|
|
294
|
+
if (val > 1e9) return Math.max(0, val * 1000 - Date.now());
|
|
295
|
+
return Math.max(0, Math.round(val * 1000));
|
|
296
|
+
}
|
|
297
|
+
if (!val || typeof val !== "string") return null;
|
|
298
|
+
const s = val.trim();
|
|
299
|
+
|
|
300
|
+
const rawNum = Number(s);
|
|
301
|
+
if (Number.isFinite(rawNum) && rawNum >= 0) {
|
|
302
|
+
if (rawNum > 1e11) return Math.max(0, rawNum - Date.now());
|
|
303
|
+
if (rawNum > 1e9) return Math.max(0, rawNum * 1000 - Date.now());
|
|
304
|
+
return Math.max(0, Math.round(rawNum * 1000));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const when = Date.parse(s);
|
|
308
|
+
if (Number.isFinite(when) && when > Date.now()) {
|
|
309
|
+
return Math.max(0, when - Date.now());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const pattern = /([0-9]+(?:\.[0-9]+)?)\s*(milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b/gi;
|
|
313
|
+
let match;
|
|
314
|
+
let totalMs = 0;
|
|
315
|
+
let matched = false;
|
|
316
|
+
while ((match = pattern.exec(s)) !== null) {
|
|
317
|
+
matched = true;
|
|
318
|
+
const n = parseFloat(match[1]);
|
|
319
|
+
const unit = match[2].toLowerCase();
|
|
320
|
+
if (!Number.isFinite(n)) continue;
|
|
321
|
+
if (unit.startsWith("ms") || unit.startsWith("milli")) {
|
|
322
|
+
totalMs += n;
|
|
323
|
+
} else if (unit.startsWith("s")) {
|
|
324
|
+
totalMs += n * 1000;
|
|
325
|
+
} else if (unit.startsWith("m")) {
|
|
326
|
+
totalMs += n * 60 * 1000;
|
|
327
|
+
} else if (unit.startsWith("h")) {
|
|
328
|
+
totalMs += n * 3600 * 1000;
|
|
329
|
+
} else if (unit.startsWith("d")) {
|
|
330
|
+
totalMs += n * 86400 * 1000;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return matched ? Math.round(totalMs) : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Extract retry duration from unstructured error message text.
|
|
338
|
+
* Matches phrases like "retry after 12.5s", "try again in 5 seconds", "resets in 30s", "wait 10s".
|
|
339
|
+
*/
|
|
340
|
+
export function extractDurationFromText(text) {
|
|
341
|
+
if (!text || typeof text !== "string") return null;
|
|
342
|
+
|
|
343
|
+
const p1 = /(?:retry(?:ing)?|try\s+again|wait(?:ing)?|resets?|available|back\s*off)\s+(?:again\s+)?(?:after|in|for)\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b(?:\s+[0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)*|[0-9]+(?:\.[0-9]+)?)/i;
|
|
344
|
+
const m1 = text.match(p1);
|
|
345
|
+
if (m1) {
|
|
346
|
+
const parsed = parseDurationMs(m1[1]);
|
|
347
|
+
if (parsed != null) return parsed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const p2 = /(?:wait(?:ing)?)\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)/i;
|
|
351
|
+
const m2 = text.match(p2);
|
|
352
|
+
if (m2) {
|
|
353
|
+
const parsed = parseDurationMs(m2[1]);
|
|
354
|
+
if (parsed != null) return parsed;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const p3 = /in\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)\s*[,.]?\s*(?:please\s+)?(?:retry|try)/i;
|
|
358
|
+
const m3 = text.match(p3);
|
|
359
|
+
if (m3) {
|
|
360
|
+
const parsed = parseDurationMs(m3[1]);
|
|
361
|
+
if (parsed != null) return parsed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parse retry delay from response headers or error response body into milliseconds.
|
|
369
|
+
* Checks standard and provider-specific rate-limit headers as well as JSON fields
|
|
370
|
+
* and error messages returned by AI gateways (HackerRank, OpenAI, Anthropic, LiteLLM, Gemini, etc.).
|
|
371
|
+
*/
|
|
372
|
+
export function retryAfterMs(headers, bodyText) {
|
|
373
|
+
const msHeader = headers?.get?.("retry-after-ms");
|
|
374
|
+
if (msHeader) {
|
|
375
|
+
const ms = Number(msHeader);
|
|
376
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.round(ms);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const headerKeys = [
|
|
380
|
+
"retry-after",
|
|
381
|
+
"x-retry-after",
|
|
382
|
+
"x-ratelimit-reset-requests",
|
|
383
|
+
"x-ratelimit-reset-tokens",
|
|
384
|
+
"x-ratelimit-reset",
|
|
385
|
+
];
|
|
386
|
+
for (const k of headerKeys) {
|
|
387
|
+
const val = headers?.get?.(k);
|
|
388
|
+
if (val) {
|
|
389
|
+
const parsed = parseDurationMs(val);
|
|
390
|
+
if (parsed != null) return parsed;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (bodyText) {
|
|
395
|
+
let json = null;
|
|
396
|
+
if (typeof bodyText === "object") {
|
|
397
|
+
json = bodyText;
|
|
398
|
+
} else if (typeof bodyText === "string") {
|
|
399
|
+
try {
|
|
400
|
+
json = JSON.parse(bodyText);
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (json && typeof json === "object") {
|
|
405
|
+
const msCandidates = [json.retry_after_ms, json.error?.retry_after_ms];
|
|
406
|
+
for (const c of msCandidates) {
|
|
407
|
+
if (Number.isFinite(Number(c)) && Number(c) >= 0) {
|
|
408
|
+
return Math.round(Number(c));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const durationCandidates = [
|
|
413
|
+
json.retry_after,
|
|
414
|
+
json.retryAfter,
|
|
415
|
+
json.retry_after_seconds,
|
|
416
|
+
json.retry_after_sec,
|
|
417
|
+
json.reset_in,
|
|
418
|
+
json.reset_after,
|
|
419
|
+
json.reset_at,
|
|
420
|
+
json.wait_seconds,
|
|
421
|
+
json.wait_time,
|
|
422
|
+
json.error?.retry_after,
|
|
423
|
+
json.error?.retryAfter,
|
|
424
|
+
json.error?.retry_after_seconds,
|
|
425
|
+
json.error?.retry_after_sec,
|
|
426
|
+
json.error?.reset_in,
|
|
427
|
+
json.error?.reset_after,
|
|
428
|
+
json.error?.reset_at,
|
|
429
|
+
json.error?.wait_seconds,
|
|
430
|
+
json.error?.wait_time,
|
|
431
|
+
];
|
|
432
|
+
for (const c of durationCandidates) {
|
|
433
|
+
if (c != null) {
|
|
434
|
+
const parsed = parseDurationMs(c);
|
|
435
|
+
if (parsed != null) return parsed;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const msgCandidates = [
|
|
440
|
+
json.error?.message,
|
|
441
|
+
typeof json.error === "string" ? json.error : null,
|
|
442
|
+
json.message,
|
|
443
|
+
json.detail,
|
|
444
|
+
typeof json.details === "string" ? json.details : null,
|
|
445
|
+
];
|
|
446
|
+
for (const msg of msgCandidates) {
|
|
447
|
+
if (msg) {
|
|
448
|
+
const extracted = extractDurationFromText(msg);
|
|
449
|
+
if (extracted != null) return extracted;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (typeof bodyText === "string") {
|
|
455
|
+
const extracted = extractDurationFromText(bodyText);
|
|
456
|
+
if (extracted != null) return extracted;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
293
460
|
return null;
|
|
294
461
|
}
|
|
295
462
|
|