clawmoney 0.11.0 → 0.11.1
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.
|
@@ -367,9 +367,23 @@ export async function callClaudeApi(opts) {
|
|
|
367
367
|
configureRateGuard();
|
|
368
368
|
return rateGuard.run(() => doCallClaudeApi(opts));
|
|
369
369
|
}
|
|
370
|
+
// Maximum number of automatic retries on transient upstream errors
|
|
371
|
+
// (429 / 5xx). Matches the Anthropic official SDK default. Does NOT count
|
|
372
|
+
// the initial attempt or the one-shot 401-refresh retry.
|
|
373
|
+
const MAX_TRANSIENT_RETRIES = 2;
|
|
374
|
+
function parseRetryAfterMs(header) {
|
|
375
|
+
if (!header)
|
|
376
|
+
return null;
|
|
377
|
+
const asSeconds = Number(header);
|
|
378
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0)
|
|
379
|
+
return asSeconds * 1000;
|
|
380
|
+
const asDate = Date.parse(header);
|
|
381
|
+
if (Number.isFinite(asDate))
|
|
382
|
+
return Math.max(0, asDate - Date.now());
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
370
385
|
async function doCallClaudeApi(opts) {
|
|
371
386
|
const fingerprint = loadFingerprint();
|
|
372
|
-
const creds = await getFreshCreds();
|
|
373
387
|
const sessionId = randomUUID();
|
|
374
388
|
const maxTokens = opts.maxTokens ?? 4096;
|
|
375
389
|
const body = {
|
|
@@ -394,46 +408,55 @@ async function doCallClaudeApi(opts) {
|
|
|
394
408
|
metadata: { user_id: buildMetadataUserID(fingerprint, sessionId) },
|
|
395
409
|
stream: false,
|
|
396
410
|
};
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"x-claude-code-session-id": sessionId,
|
|
404
|
-
},
|
|
405
|
-
body: JSON.stringify(body),
|
|
406
|
-
});
|
|
407
|
-
if (resp.status === 401) {
|
|
408
|
-
// Token became invalid mid-flight; force a refresh and retry once.
|
|
409
|
-
logger.warn("[claude-api] 401 from upstream, forcing refresh + retry");
|
|
410
|
-
cachedCreds = null;
|
|
411
|
-
const fresh = await getFreshCreds();
|
|
412
|
-
const retry = await fetch(ANTHROPIC_MESSAGES_URL, {
|
|
411
|
+
const bodyJson = JSON.stringify(body);
|
|
412
|
+
let transientAttempt = 0;
|
|
413
|
+
let hasRefreshed = false;
|
|
414
|
+
while (true) {
|
|
415
|
+
const creds = await getFreshCreds();
|
|
416
|
+
const resp = await fetch(ANTHROPIC_MESSAGES_URL, {
|
|
413
417
|
method: "POST",
|
|
414
418
|
headers: {
|
|
415
419
|
...STATIC_CLAUDE_CODE_HEADERS,
|
|
416
420
|
"user-agent": fingerprint.user_agent,
|
|
417
|
-
"authorization": `Bearer ${
|
|
421
|
+
"authorization": `Bearer ${creds.accessToken}`,
|
|
418
422
|
"x-claude-code-session-id": sessionId,
|
|
419
423
|
},
|
|
420
|
-
body:
|
|
424
|
+
body: bodyJson,
|
|
421
425
|
});
|
|
422
|
-
if (
|
|
423
|
-
const
|
|
424
|
-
|
|
426
|
+
if (resp.ok) {
|
|
427
|
+
const parsed = parseResponse(await resp.json(), opts.model);
|
|
428
|
+
recordSpendFromUsage(parsed, opts.model);
|
|
429
|
+
return parsed;
|
|
425
430
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
431
|
+
const errText = await resp.text();
|
|
432
|
+
// 401 → one-shot token refresh + retry. If we already refreshed once
|
|
433
|
+
// and still got 401, the credentials are genuinely broken — bubble up.
|
|
434
|
+
if (resp.status === 401 && !hasRefreshed) {
|
|
435
|
+
logger.warn("[claude-api] 401 from upstream, refreshing token + retry");
|
|
436
|
+
hasRefreshed = true;
|
|
437
|
+
cachedCreds = null;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// 429 / 5xx → transient upstream hiccup. Retry with exponential backoff
|
|
441
|
+
// + jitter, honoring Retry-After if present. This is what Anthropic's
|
|
442
|
+
// official SDK does by default; buyers used to see these as hard 502s
|
|
443
|
+
// even when the right move was "wait 1s and try again". We only do this
|
|
444
|
+
// inside the rate-guard slot we're already holding, so retries don't
|
|
445
|
+
// re-queue behind other requests.
|
|
446
|
+
const isTransient = resp.status === 429 ||
|
|
447
|
+
(resp.status >= 500 && resp.status <= 599);
|
|
448
|
+
if (isTransient && transientAttempt < MAX_TRANSIENT_RETRIES) {
|
|
449
|
+
const retryAfter = parseRetryAfterMs(resp.headers.get("retry-after"));
|
|
450
|
+
const backoffMs = retryAfter ?? 500 * Math.pow(2, transientAttempt) + Math.random() * 500;
|
|
451
|
+
logger.warn(`[claude-api] ${resp.status} from upstream (attempt ${transientAttempt + 1}/${MAX_TRANSIENT_RETRIES + 1}), retrying in ${Math.round(backoffMs)}ms — ${errText.slice(0, 200)}`);
|
|
452
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
453
|
+
transientAttempt++;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
// Unrecoverable — bubble up with the upstream status + body so Hub can
|
|
457
|
+
// translate it into a sensible HTTP status for the buyer.
|
|
458
|
+
throw new Error(`Anthropic ${resp.status}: ${errText.slice(0, 400)}`);
|
|
433
459
|
}
|
|
434
|
-
const parsed = parseResponse(await resp.json(), opts.model);
|
|
435
|
-
recordSpendFromUsage(parsed, opts.model);
|
|
436
|
-
return parsed;
|
|
437
460
|
}
|
|
438
461
|
function recordSpendFromUsage(parsed, model) {
|
|
439
462
|
if (!rateGuard)
|