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 resp = await fetch(ANTHROPIC_MESSAGES_URL, {
398
- method: "POST",
399
- headers: {
400
- ...STATIC_CLAUDE_CODE_HEADERS,
401
- "user-agent": fingerprint.user_agent,
402
- "authorization": `Bearer ${creds.accessToken}`,
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 ${fresh.accessToken}`,
421
+ "authorization": `Bearer ${creds.accessToken}`,
418
422
  "x-claude-code-session-id": sessionId,
419
423
  },
420
- body: JSON.stringify(body),
424
+ body: bodyJson,
421
425
  });
422
- if (!retry.ok) {
423
- const text = await retry.text();
424
- throw new Error(`Anthropic ${retry.status} after refresh: ${text.slice(0, 400)}`);
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 parsedRetry = parseResponse(await retry.json(), opts.model);
427
- recordSpendFromUsage(parsedRetry, opts.model);
428
- return parsedRetry;
429
- }
430
- if (!resp.ok) {
431
- const text = await resp.text();
432
- throw new Error(`Anthropic ${resp.status}: ${text.slice(0, 400)}`);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {