@juspay/neurolink 11.29.2 → 11.30.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/CHANGELOG.md +3 -3
- package/dist/auth/anthropicOAuth.d.ts +50 -0
- package/dist/auth/anthropicOAuth.js +78 -0
- package/dist/browser/neurolink.min.js +393 -393
- package/dist/cli/commands/proxy.d.ts +2 -0
- package/dist/cli/commands/proxy.js +284 -4
- package/dist/cli/commands/proxyExpose.d.ts +35 -0
- package/dist/cli/commands/proxyExpose.js +252 -0
- package/dist/cli/commands/proxyPeer.d.ts +29 -0
- package/dist/cli/commands/proxyPeer.js +738 -0
- package/dist/cli/commands/proxyShare.d.ts +37 -0
- package/dist/cli/commands/proxyShare.js +1080 -0
- package/dist/cli/parser.js +7 -1
- package/dist/proxy/peerStore.d.ts +52 -0
- package/dist/proxy/peerStore.js +324 -0
- package/dist/proxy/peerTransport.d.ts +38 -0
- package/dist/proxy/peerTransport.js +242 -0
- package/dist/proxy/proxyPaths.d.ts +8 -0
- package/dist/proxy/proxyPaths.js +55 -17
- package/dist/proxy/requestLogger.js +8 -0
- package/dist/proxy/residentGrants.d.ts +57 -0
- package/dist/proxy/residentGrants.js +393 -0
- package/dist/proxy/shareAudit.d.ts +81 -0
- package/dist/proxy/shareAudit.js +280 -0
- package/dist/proxy/shareContext.d.ts +38 -0
- package/dist/proxy/shareContext.js +92 -0
- package/dist/proxy/shareGate.d.ts +64 -0
- package/dist/proxy/shareGate.js +216 -0
- package/dist/proxy/shareGrants.d.ts +115 -0
- package/dist/proxy/shareGrants.js +590 -0
- package/dist/proxy/shareLease.d.ts +101 -0
- package/dist/proxy/shareLease.js +192 -0
- package/dist/proxy/shareLedger.d.ts +105 -0
- package/dist/proxy/shareLedger.js +406 -0
- package/dist/proxy/shareListener.d.ts +60 -0
- package/dist/proxy/shareListener.js +143 -0
- package/dist/proxy/shareNotes.d.ts +97 -0
- package/dist/proxy/shareNotes.js +234 -0
- package/dist/proxy/sharePolicy.d.ts +110 -0
- package/dist/proxy/sharePolicy.js +366 -0
- package/dist/proxy/shareProvisioning.d.ts +110 -0
- package/dist/proxy/shareProvisioning.js +237 -0
- package/dist/proxy/shareReceipts.d.ts +99 -0
- package/dist/proxy/shareReceipts.js +303 -0
- package/dist/proxy/shareSigning.d.ts +40 -0
- package/dist/proxy/shareSigning.js +78 -0
- package/dist/server/routes/claudeProxyRoutes.js +1066 -3
- package/dist/types/cli.d.ts +61 -0
- package/dist/types/proxy.d.ts +781 -0
- package/package.json +2 -1
|
@@ -32,12 +32,26 @@ import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.j
|
|
|
32
32
|
import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
|
|
33
33
|
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
34
34
|
import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
|
|
35
|
+
import { selectBorrowablePeers } from "../../proxy/peerStore.js";
|
|
36
|
+
import { evaluateResidentAccount, getResidentGrantForAccount, recordResidentSpend, } from "../../proxy/residentGrants.js";
|
|
37
|
+
import { buildProvisionClaim, isLeaseRefusal, issueLease, } from "../../proxy/shareLease.js";
|
|
38
|
+
import { forwardToPeer } from "../../proxy/peerTransport.js";
|
|
39
|
+
import { getShareContext } from "../../proxy/shareContext.js";
|
|
40
|
+
import { buildShareRefusal, extractShareToken } from "../../proxy/shareGate.js";
|
|
41
|
+
import { debitShareGrantCoins, getNodePublicUrl, getNoteSecret, getShareGrant, resolveShareToken, setShareGrantState, } from "../../proxy/shareGrants.js";
|
|
42
|
+
import { recordAuditObservation } from "../../proxy/shareAudit.js";
|
|
43
|
+
import { claimProvisionRequest, openProvisionRequest, } from "../../proxy/shareProvisioning.js";
|
|
44
|
+
import { applyReciprocalNetting, listShareReceipts, } from "../../proxy/shareReceipts.js";
|
|
45
|
+
import { decodeShareNote, inspectShareNote, redeemShareNote, } from "../../proxy/shareNotes.js";
|
|
46
|
+
import { verifySharePayload } from "../../proxy/shareSigning.js";
|
|
47
|
+
import { availableCoins, readSharePoolWindowUsage, readShareWindowUsage, recordShareWindowDelta, settleShareUsage, usageToCoins, } from "../../proxy/shareLedger.js";
|
|
48
|
+
import { accountsInGrantScope, filterAccountsForGrant, isModelAllowed, isWithinSchedule, shareRefusalStatus, summarizeAccountExclusions, } from "../../proxy/sharePolicy.js";
|
|
35
49
|
import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
|
|
36
50
|
import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
|
|
37
51
|
import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
|
|
38
52
|
import { normalizeMaxInflightPerAccount } from "../../proxy/modelRouter.js";
|
|
39
53
|
import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
|
|
40
|
-
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
|
|
54
|
+
import { getAccountStats, recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
|
|
41
55
|
import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
42
56
|
import { logger } from "../../utils/logger.js";
|
|
43
57
|
import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
|
|
@@ -482,7 +496,10 @@ function publishLimitHeaders(ctx, args) {
|
|
|
482
496
|
...(pool ? { pool } : {}),
|
|
483
497
|
},
|
|
484
498
|
});
|
|
485
|
-
ctx.responseHeaders = {
|
|
499
|
+
ctx.responseHeaders = {
|
|
500
|
+
...(ctx.responseHeaders ?? {}),
|
|
501
|
+
...redactHeadersForBorrower(headers),
|
|
502
|
+
};
|
|
486
503
|
}
|
|
487
504
|
catch (error) {
|
|
488
505
|
// Diagnostics must never break a response that is otherwise fine.
|
|
@@ -2457,6 +2474,540 @@ async function handleClaudePassthroughJsonResponse(args) {
|
|
|
2457
2474
|
}
|
|
2458
2475
|
return responseJson;
|
|
2459
2476
|
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Narrow the pool to what the current borrowed request's grant may use.
|
|
2479
|
+
*
|
|
2480
|
+
* A no-op — and one map lookup — for the node's own traffic, which is every
|
|
2481
|
+
* request on a proxy that shares nothing.
|
|
2482
|
+
*
|
|
2483
|
+
* When the filter empties the pool the refusal must be share-shaped, not the
|
|
2484
|
+
* generic "no credentials" 401: the borrower has perfectly good credentials,
|
|
2485
|
+
* the lender is simply holding capacity back. Saying it precisely is what lets
|
|
2486
|
+
* the borrower fall through to another peer instead of re-authenticating.
|
|
2487
|
+
*/
|
|
2488
|
+
async function applyShareAccountGates(args) {
|
|
2489
|
+
const share = getShareContext();
|
|
2490
|
+
if (!share) {
|
|
2491
|
+
return { accounts: args.accounts };
|
|
2492
|
+
}
|
|
2493
|
+
if (args.accounts.length === 0) {
|
|
2494
|
+
// Nothing to withhold. Claiming a share refusal here would tell the
|
|
2495
|
+
// borrower the lender is holding capacity back, when in fact the lender has
|
|
2496
|
+
// no usable accounts at all — a different problem with a different fix,
|
|
2497
|
+
// and the existing no-credentials path already words it correctly.
|
|
2498
|
+
return { accounts: args.accounts };
|
|
2499
|
+
}
|
|
2500
|
+
const now = Date.now();
|
|
2501
|
+
const views = await Promise.all(args.accounts.map(async (account) => {
|
|
2502
|
+
const quota = getOrCreateRuntimeState(account.key).quota;
|
|
2503
|
+
const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
|
|
2504
|
+
const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
|
|
2505
|
+
const borrowed = await readShareWindowUsage(share.grantId, account.key, sessionResetAt, weeklyResetAt);
|
|
2506
|
+
return {
|
|
2507
|
+
accountKey: account.key,
|
|
2508
|
+
sessionUsed: quota?.sessionUsed ?? null,
|
|
2509
|
+
weeklyUsed: quota?.weeklyUsed ?? null,
|
|
2510
|
+
sessionResetAt,
|
|
2511
|
+
weeklyResetAt,
|
|
2512
|
+
borrowedSessionFraction: borrowed.sessionFraction,
|
|
2513
|
+
borrowedWeeklyFraction: borrowed.weeklyFraction,
|
|
2514
|
+
};
|
|
2515
|
+
}));
|
|
2516
|
+
const grant = await getShareGrant(share.grantId);
|
|
2517
|
+
if (!grant) {
|
|
2518
|
+
return { accounts: args.accounts };
|
|
2519
|
+
}
|
|
2520
|
+
// Pool-wide first: how much of the pool this grant has already taken. The
|
|
2521
|
+
// denominator is the accounts the grant may actually draw on — an
|
|
2522
|
+
// `--accounts`-restricted grant divides by those, not by every credential
|
|
2523
|
+
// this node holds, or its ceiling would scale with the size of a pool it
|
|
2524
|
+
// cannot reach.
|
|
2525
|
+
const poolUsage = await readSharePoolWindowUsage(share.grantId, accountsInGrantScope(grant.gates, views).inScope);
|
|
2526
|
+
const decision = filterAccountsForGrant(grant, views, now, poolUsage);
|
|
2527
|
+
const allowed = new Set(decision.allowed);
|
|
2528
|
+
const survivors = args.accounts.filter((account) => allowed.has(account.key));
|
|
2529
|
+
if (survivors.length > 0) {
|
|
2530
|
+
return { accounts: survivors };
|
|
2531
|
+
}
|
|
2532
|
+
const reason = summarizeAccountExclusions(decision.excluded);
|
|
2533
|
+
const refusal = buildShareRefusal(reason, {
|
|
2534
|
+
status: shareRefusalStatus(reason),
|
|
2535
|
+
grant,
|
|
2536
|
+
retryAfterSeconds: earliestShareRecoverySeconds(views, now),
|
|
2537
|
+
});
|
|
2538
|
+
// Assigned, not merged into an existing object: a refusal is often the first
|
|
2539
|
+
// thing to touch this context, and only copying when headers already existed
|
|
2540
|
+
// meant the borrower learned nothing about why it was refused.
|
|
2541
|
+
args.ctx.responseHeaders = {
|
|
2542
|
+
...(args.ctx.responseHeaders ?? {}),
|
|
2543
|
+
...refusal.headers,
|
|
2544
|
+
};
|
|
2545
|
+
logger.always(`[proxy] share ${share.peerLabel} withheld: ${reason} (${decision.excluded.length} accounts)`);
|
|
2546
|
+
return {
|
|
2547
|
+
accounts: [],
|
|
2548
|
+
refusal: {
|
|
2549
|
+
response: args.buildLoggedClaudeError(refusal.status, refusal.body.error.message, refusal.body.error.type),
|
|
2550
|
+
status: refusal.status,
|
|
2551
|
+
message: refusal.body.error.message,
|
|
2552
|
+
},
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Withhold credentials a lender provisioned here whose lease has lapsed.
|
|
2557
|
+
*
|
|
2558
|
+
* A complete-mode credential keeps working while the lender is unreachable —
|
|
2559
|
+
* that is what it is for — but only for as long as the lease allows. Past that,
|
|
2560
|
+
* the honest thing is to stop using it, and the borrower is the only party in a
|
|
2561
|
+
* position to do so.
|
|
2562
|
+
*
|
|
2563
|
+
* A no-op for a node with no resident grants, which is every node until someone
|
|
2564
|
+
* provisions one.
|
|
2565
|
+
*/
|
|
2566
|
+
async function dropExpiredResidentAccounts(accounts) {
|
|
2567
|
+
const survivors = [];
|
|
2568
|
+
const withheld = [];
|
|
2569
|
+
for (const account of accounts) {
|
|
2570
|
+
const verdict = await evaluateResidentAccount(account.key);
|
|
2571
|
+
if (!verdict) {
|
|
2572
|
+
survivors.push(account);
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
if (!isLeaseRefusal(verdict)) {
|
|
2576
|
+
survivors.push(account);
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
logger.always(`[proxy] withholding leased account=${account.label}: ${verdict.reason}`);
|
|
2580
|
+
withheld.push({ label: account.label, reason: verdict.reason });
|
|
2581
|
+
}
|
|
2582
|
+
return { accounts: survivors, withheld };
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Enforce a lender's model allowlist on the credential they provisioned here.
|
|
2586
|
+
*
|
|
2587
|
+
* A live share checks this at the lender's gate. A complete share has no gate in
|
|
2588
|
+
* the request path, so the borrower has to hold the line itself — otherwise a
|
|
2589
|
+
* grant that says "Sonnet only" becomes "anything" the moment it is provisioned,
|
|
2590
|
+
* which is exactly the property that would make complete mode unshippable.
|
|
2591
|
+
*/
|
|
2592
|
+
async function dropLeaseDisallowedAccounts(accounts, requestedModel) {
|
|
2593
|
+
const survivors = [];
|
|
2594
|
+
const withheld = [];
|
|
2595
|
+
const now = Date.now();
|
|
2596
|
+
for (const account of accounts) {
|
|
2597
|
+
const resident = await getResidentGrantForAccount(account.key);
|
|
2598
|
+
if (!resident) {
|
|
2599
|
+
survivors.push(account);
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
const gates = resident.lease.gates;
|
|
2603
|
+
// Request-level gates first: they do not depend on any window figure, and
|
|
2604
|
+
// saying "this share does not cover Opus" is more useful than saying the
|
|
2605
|
+
// slice is spent when both are true.
|
|
2606
|
+
if (!isModelAllowed(gates.models, requestedModel)) {
|
|
2607
|
+
logger.always(`[proxy] leased account=${account.label} does not cover model ${sanitizeForLog(requestedModel ?? "unknown")}`);
|
|
2608
|
+
withheld.push({ label: account.label, reason: "model_not_allowed" });
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (gates.schedule && !isWithinSchedule(gates.schedule, now)) {
|
|
2612
|
+
logger.always(`[proxy] leased account=${account.label} is outside its allowed hours`);
|
|
2613
|
+
withheld.push({ label: account.label, reason: "out_of_window" });
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
// Account-level gates through the lender's own evaluator, on the lender's
|
|
2617
|
+
// own numbers. A resident credential is minted from exactly one account, so
|
|
2618
|
+
// the pool of the pool-wide slice is that one account and the formula
|
|
2619
|
+
// collapses to the per-account case — which is precisely why the same code
|
|
2620
|
+
// can serve both sides.
|
|
2621
|
+
const quota = getOrCreateRuntimeState(account.key).quota;
|
|
2622
|
+
const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
|
|
2623
|
+
const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
|
|
2624
|
+
const borrowed = await readShareWindowUsage(resident.grantId, account.key, sessionResetAt, weeklyResetAt);
|
|
2625
|
+
const view = {
|
|
2626
|
+
accountKey: account.key,
|
|
2627
|
+
sessionUsed: quota?.sessionUsed ?? null,
|
|
2628
|
+
weeklyUsed: quota?.weeklyUsed ?? null,
|
|
2629
|
+
sessionResetAt,
|
|
2630
|
+
weeklyResetAt,
|
|
2631
|
+
borrowedSessionFraction: borrowed.sessionFraction,
|
|
2632
|
+
borrowedWeeklyFraction: borrowed.weeklyFraction,
|
|
2633
|
+
};
|
|
2634
|
+
const poolUsage = await readSharePoolWindowUsage(resident.grantId, [view]);
|
|
2635
|
+
// `gates.accounts` names the *lender's* accounts and means nothing here —
|
|
2636
|
+
// the credential this lease governs is the only account in scope by
|
|
2637
|
+
// construction. Carrying it over would filter the account out by its local
|
|
2638
|
+
// label and withhold every leased request.
|
|
2639
|
+
const { accounts: _lenderAccounts, ...localGates } = gates;
|
|
2640
|
+
const decision = filterAccountsForGrant(leaseAsGrant(resident, localGates), [view], now, poolUsage);
|
|
2641
|
+
if (decision.allowed.length === 0) {
|
|
2642
|
+
const reason = summarizeAccountExclusions(decision.excluded);
|
|
2643
|
+
logger.always(`[proxy] withholding leased account=${account.label}: ${reason}`);
|
|
2644
|
+
withheld.push({ label: account.label, reason });
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
survivors.push(account);
|
|
2648
|
+
}
|
|
2649
|
+
return { accounts: survivors, withheld };
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Dress a lease up as the grant its gates came from.
|
|
2653
|
+
*
|
|
2654
|
+
* The borrower has no grant record — only the signed projection of one — but the
|
|
2655
|
+
* admission evaluator takes a grant. Rebuilding one here means both sides run
|
|
2656
|
+
* the identical gate code rather than a borrower-side reimplementation that
|
|
2657
|
+
* drifts from the lender's the first time a gate is added.
|
|
2658
|
+
*/
|
|
2659
|
+
function leaseAsGrant(resident, gates) {
|
|
2660
|
+
const snapshot = resident.lease.entitlementSnapshot;
|
|
2661
|
+
return {
|
|
2662
|
+
schemaVersion: 1,
|
|
2663
|
+
id: resident.grantId,
|
|
2664
|
+
peerLabel: resident.lease.peerLabel,
|
|
2665
|
+
// Never compared: the lease's own signature is what authenticates it, and
|
|
2666
|
+
// it was checked before this account was allowed to reach here at all.
|
|
2667
|
+
tokenHash: "",
|
|
2668
|
+
tokenSalt: "",
|
|
2669
|
+
level: "complete",
|
|
2670
|
+
state: "active",
|
|
2671
|
+
entitlement: snapshot === "unlimited"
|
|
2672
|
+
? { ledger: "unlimited" }
|
|
2673
|
+
: { ledger: "coins", coins: snapshot },
|
|
2674
|
+
gates,
|
|
2675
|
+
createdAt: resident.lease.issuedAt,
|
|
2676
|
+
updatedAt: resident.lease.issuedAt,
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Attribute a window movement to the grant that caused it.
|
|
2681
|
+
*
|
|
2682
|
+
* A no-op for the node's own traffic. Fire-and-forget: bookkeeping must never
|
|
2683
|
+
* delay or fail a response the borrower is already receiving.
|
|
2684
|
+
*/
|
|
2685
|
+
function recordBorrowedWindowDelta(accountKey, before, after) {
|
|
2686
|
+
const now = Date.now();
|
|
2687
|
+
const observe = (grantId) => {
|
|
2688
|
+
void recordShareWindowDelta({
|
|
2689
|
+
grantId,
|
|
2690
|
+
accountKey,
|
|
2691
|
+
sessionBefore: before?.sessionUsed ?? null,
|
|
2692
|
+
sessionAfter: after.sessionUsed,
|
|
2693
|
+
sessionResetAt: resetEpochToMs(after.sessionResetAt, now) ?? null,
|
|
2694
|
+
weeklyBefore: before?.weeklyUsed ?? null,
|
|
2695
|
+
weeklyAfter: after.weeklyUsed,
|
|
2696
|
+
weeklyResetAt: resetEpochToMs(after.weeklyResetAt, now) ?? null,
|
|
2697
|
+
});
|
|
2698
|
+
};
|
|
2699
|
+
const share = getShareContext();
|
|
2700
|
+
if (share) {
|
|
2701
|
+
observe(share.grantId);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
// Not borrowed *from* us — but it may be borrowed *by* us. A leased account's
|
|
2705
|
+
// windows move because this node spent them, and the lease's slice ceiling has
|
|
2706
|
+
// nothing to measure unless that movement is recorded against the grant. The
|
|
2707
|
+
// lender keeps the same book on its side; both read it with the same formula.
|
|
2708
|
+
void getResidentGrantForAccount(accountKey)
|
|
2709
|
+
.then((resident) => {
|
|
2710
|
+
if (resident) {
|
|
2711
|
+
observe(resident.grantId);
|
|
2712
|
+
}
|
|
2713
|
+
})
|
|
2714
|
+
.catch(() => {
|
|
2715
|
+
// Bookkeeping only — never fail a response the client already has.
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Charge a completed borrowed request against its grant.
|
|
2720
|
+
*
|
|
2721
|
+
* A no-op for the node's own traffic, and fire-and-forget for the same reason
|
|
2722
|
+
* as the window delta above.
|
|
2723
|
+
*/
|
|
2724
|
+
/**
|
|
2725
|
+
* Bill a request served by a credential a lender provisioned here.
|
|
2726
|
+
*
|
|
2727
|
+
* The mirror of `settleBorrowedRequest`: that one charges a borrower on the
|
|
2728
|
+
* lender's node, this one records what *this* node owes a lender whose
|
|
2729
|
+
* credential it is holding. Without it a complete-mode heartbeat reports zero
|
|
2730
|
+
* forever and the lender's balance never moves.
|
|
2731
|
+
*/
|
|
2732
|
+
function recordLeasedAccountSpend(accountLabel, model, usage) {
|
|
2733
|
+
const coins = usageToCoins({
|
|
2734
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
2735
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
2736
|
+
...(usage.cacheCreationTokens !== undefined
|
|
2737
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
2738
|
+
: {}),
|
|
2739
|
+
...(usage.cacheReadTokens !== undefined
|
|
2740
|
+
? { cacheReadTokens: usage.cacheReadTokens }
|
|
2741
|
+
: {}),
|
|
2742
|
+
}, model);
|
|
2743
|
+
void recordResidentSpend(accountLabel, coins).catch(() => {
|
|
2744
|
+
// Bookkeeping only — never fail a response the client already has.
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Charge a served non-streaming response to whichever ledger owns the account.
|
|
2749
|
+
*
|
|
2750
|
+
* Both sides are no-ops unless the account is actually borrowed or leased, so
|
|
2751
|
+
* this is safe to call on every response — which is the point: gating it on
|
|
2752
|
+
* anything else is how it came to be skipped.
|
|
2753
|
+
*/
|
|
2754
|
+
function settleFromResponseUsage(account, responseJson) {
|
|
2755
|
+
if (!responseJson || typeof responseJson !== "object") {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const usage = responseJson.usage;
|
|
2759
|
+
if (!usage) {
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
const model = servedModelName(responseJson);
|
|
2763
|
+
const tokens = {
|
|
2764
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
2765
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
2766
|
+
cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
|
|
2767
|
+
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
2768
|
+
};
|
|
2769
|
+
settleBorrowedRequest(account.key, model, tokens);
|
|
2770
|
+
recordLeasedAccountSpend(account.label, model, tokens);
|
|
2771
|
+
}
|
|
2772
|
+
function settleBorrowedRequest(accountKey, model, usage) {
|
|
2773
|
+
const share = getShareContext();
|
|
2774
|
+
if (!share) {
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
void settleShareUsage({
|
|
2778
|
+
grantId: share.grantId,
|
|
2779
|
+
accountKey,
|
|
2780
|
+
model: model ?? share.model,
|
|
2781
|
+
usage: {
|
|
2782
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
2783
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
2784
|
+
...(usage.cacheCreationTokens !== undefined
|
|
2785
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
2786
|
+
: {}),
|
|
2787
|
+
...(usage.cacheReadTokens !== undefined
|
|
2788
|
+
? { cacheReadTokens: usage.cacheReadTokens }
|
|
2789
|
+
: {}),
|
|
2790
|
+
},
|
|
2791
|
+
...(share.holdId ? { holdId: share.holdId } : {}),
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Remove what a borrower has no business seeing.
|
|
2796
|
+
*
|
|
2797
|
+
* `x-neurolink-account` carries the lender's account label, which for an OAuth
|
|
2798
|
+
* account is their email address; the pool counters describe the shape of a
|
|
2799
|
+
* pool that is not the borrower's. The borrower's own routing needs the quota
|
|
2800
|
+
* and grant headers, and nothing else here.
|
|
2801
|
+
*
|
|
2802
|
+
* A no-op for the node's own traffic, where these headers are exactly the
|
|
2803
|
+
* diagnostics the operator wants.
|
|
2804
|
+
*/
|
|
2805
|
+
function redactHeadersForBorrower(headers) {
|
|
2806
|
+
if (!getShareContext()) {
|
|
2807
|
+
return headers;
|
|
2808
|
+
}
|
|
2809
|
+
const redacted = {};
|
|
2810
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2811
|
+
const lower = key.toLowerCase();
|
|
2812
|
+
if (lower === "x-neurolink-account" ||
|
|
2813
|
+
lower === "x-neurolink-account-type") {
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
if (lower.startsWith("x-neurolink-pool-")) {
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
redacted[key] = value;
|
|
2820
|
+
}
|
|
2821
|
+
return redacted;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Record a heartbeat against the account's real utilization, and pause the grant
|
|
2825
|
+
* when the two have disagreed too many times in a row.
|
|
2826
|
+
*
|
|
2827
|
+
* The lender's own request count on that account is read from its usage stats:
|
|
2828
|
+
* an interval this node also used is not evidence about anybody, so the audit
|
|
2829
|
+
* abstains rather than guessing.
|
|
2830
|
+
*/
|
|
2831
|
+
async function auditCompleteShareHeartbeat(grant, reportedCoins) {
|
|
2832
|
+
const accountLabel = grant.provisionedAccount;
|
|
2833
|
+
if (!accountLabel) {
|
|
2834
|
+
// Nothing to audit against: the grant predates provisioning or was attached
|
|
2835
|
+
// by hand. Say nothing rather than inventing a baseline.
|
|
2836
|
+
return { paused: false, detail: "no provisioned account recorded" };
|
|
2837
|
+
}
|
|
2838
|
+
const state = accountRuntimeState.get(`anthropic:${accountLabel}`);
|
|
2839
|
+
const stats = getAccountStats(accountLabel);
|
|
2840
|
+
const { verdict, shouldPause } = await recordAuditObservation({
|
|
2841
|
+
grantId: grant.id,
|
|
2842
|
+
accountLabel,
|
|
2843
|
+
lenderRequestsTotal: stats?.successCount ?? 0,
|
|
2844
|
+
observation: {
|
|
2845
|
+
at: Date.now(),
|
|
2846
|
+
sessionUsed: state?.quota?.sessionUsed ?? null,
|
|
2847
|
+
weeklyUsed: state?.quota?.weeklyUsed ?? null,
|
|
2848
|
+
reportedCoins,
|
|
2849
|
+
// Replaced with the per-interval delta by the recorder.
|
|
2850
|
+
lenderRequests: 0,
|
|
2851
|
+
},
|
|
2852
|
+
});
|
|
2853
|
+
if (shouldPause) {
|
|
2854
|
+
await setShareGrantState(grant.id, "paused");
|
|
2855
|
+
return {
|
|
2856
|
+
paused: true,
|
|
2857
|
+
detail: verdict.drifted ? verdict.detail : "repeated unexplained usage",
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
paused: false,
|
|
2862
|
+
detail: verdict.drifted ? verdict.detail : "consistent",
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Apply a complete-mode borrower's self-reported spend to their balance.
|
|
2867
|
+
*
|
|
2868
|
+
* Deliberately trusting at this layer and deliberately verified elsewhere: the
|
|
2869
|
+
* lender cannot see a resident credential's traffic directly, but it can see the
|
|
2870
|
+
* account's true utilization through the usage API, so under-reporting shows up
|
|
2871
|
+
* as drift rather than as free capacity.
|
|
2872
|
+
*/
|
|
2873
|
+
async function recordReportedResidentSpend(grant, coins) {
|
|
2874
|
+
await debitShareGrantCoins(grant.id, coins);
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Compare a presented secret against the stored one without leaking where they
|
|
2878
|
+
* diverge. Hand-rolled rather than `crypto.timingSafeEqual` for the same reason
|
|
2879
|
+
* as `shareGrants.digestsMatch`: this module is reachable from a build whose
|
|
2880
|
+
* `node:crypto` stub does not carry it.
|
|
2881
|
+
*/
|
|
2882
|
+
function secretsMatch(expected, presented) {
|
|
2883
|
+
if (expected.length !== presented.length || expected.length === 0) {
|
|
2884
|
+
return false;
|
|
2885
|
+
}
|
|
2886
|
+
let difference = 0;
|
|
2887
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
2888
|
+
difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
|
|
2889
|
+
}
|
|
2890
|
+
return difference === 0;
|
|
2891
|
+
}
|
|
2892
|
+
/**
|
|
2893
|
+
* Peer wire protocol version. Bumped when a borrower would misread an older
|
|
2894
|
+
* node's answers — not when a field is added, which every version tolerates.
|
|
2895
|
+
*/
|
|
2896
|
+
const PEER_PROTOCOL_VERSION = 1;
|
|
2897
|
+
/** What this node can do for a peer, so a borrower need not probe to find out. */
|
|
2898
|
+
const PEER_CAPABILITIES = ["live", "complete", "handshake", "limits"];
|
|
2899
|
+
/**
|
|
2900
|
+
* What a borrower may know about the lender's pool.
|
|
2901
|
+
*
|
|
2902
|
+
* Deliberately shaped as "what is left for *you*", never "what the lender has":
|
|
2903
|
+
* no labels, no per-account figures, no counts that would let a borrower infer
|
|
2904
|
+
* how many credentials sit behind the tunnel. Enough to route on and nothing
|
|
2905
|
+
* more, which is what `/peer/limits` is for.
|
|
2906
|
+
*/
|
|
2907
|
+
async function buildPeerLimitsSnapshot(grant, allowlist) {
|
|
2908
|
+
const now = Date.now();
|
|
2909
|
+
const accounts = await listAnthropicAccountsForUsage(allowlist);
|
|
2910
|
+
const views = await Promise.all(accounts.map(async (account) => {
|
|
2911
|
+
const quota = getOrCreateRuntimeState(account.key).quota;
|
|
2912
|
+
const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
|
|
2913
|
+
const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
|
|
2914
|
+
const borrowed = await readShareWindowUsage(grant.id, account.key, sessionResetAt, weeklyResetAt);
|
|
2915
|
+
return {
|
|
2916
|
+
accountKey: account.key,
|
|
2917
|
+
sessionUsed: quota?.sessionUsed ?? null,
|
|
2918
|
+
weeklyUsed: quota?.weeklyUsed ?? null,
|
|
2919
|
+
sessionResetAt,
|
|
2920
|
+
weeklyResetAt,
|
|
2921
|
+
borrowedSessionFraction: borrowed.sessionFraction,
|
|
2922
|
+
borrowedWeeklyFraction: borrowed.weeklyFraction,
|
|
2923
|
+
};
|
|
2924
|
+
}));
|
|
2925
|
+
const inScope = accountsInGrantScope(grant.gates, views).inScope;
|
|
2926
|
+
const poolUsage = await readSharePoolWindowUsage(grant.id, inScope);
|
|
2927
|
+
const decision = filterAccountsForGrant(grant, views, now, poolUsage);
|
|
2928
|
+
const left = (ceiling, taken) => ceiling === undefined ? null : Math.max(0, ceiling - taken * 100);
|
|
2929
|
+
return {
|
|
2930
|
+
grantState: grant.state,
|
|
2931
|
+
level: grant.level,
|
|
2932
|
+
ledger: grant.entitlement.ledger,
|
|
2933
|
+
...(grant.entitlement.ledger === "coins"
|
|
2934
|
+
? { remainingCoins: Math.max(0, Math.floor(availableCoins(grant))) }
|
|
2935
|
+
: {}),
|
|
2936
|
+
servable: decision.allowed.length > 0,
|
|
2937
|
+
...(decision.allowed.length === 0 && decision.excluded.length > 0
|
|
2938
|
+
? { withheldReason: summarizeAccountExclusions(decision.excluded) }
|
|
2939
|
+
: {}),
|
|
2940
|
+
sliceLeftPct: {
|
|
2941
|
+
session: left(grant.gates.maxSlice?.session5hPct, poolUsage.sessionFraction),
|
|
2942
|
+
weekly: left(grant.gates.maxSlice?.weekly7dPct, poolUsage.weeklyFraction),
|
|
2943
|
+
},
|
|
2944
|
+
...(decision.allowed.length === 0
|
|
2945
|
+
? (() => {
|
|
2946
|
+
const retry = earliestShareRecoverySeconds(views, now);
|
|
2947
|
+
return retry === undefined ? {} : { retryAfterSeconds: retry };
|
|
2948
|
+
})()
|
|
2949
|
+
: {}),
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Resolve the share token on a `/peer/*` call.
|
|
2954
|
+
*
|
|
2955
|
+
* These routes sit outside the request gate — they consume no capacity, and
|
|
2956
|
+
* running them through it would spend the grant's rate allowance on a call that
|
|
2957
|
+
* exists to ask whether spending is possible — so each one authenticates itself.
|
|
2958
|
+
*/
|
|
2959
|
+
/**
|
|
2960
|
+
* Narrow a peer-auth outcome to its refusing half.
|
|
2961
|
+
*
|
|
2962
|
+
* Explicit rather than `!auth.ok`, because one of the package's build steps
|
|
2963
|
+
* compiles this file without `strictNullChecks`, where TypeScript will not
|
|
2964
|
+
* narrow a boolean discriminant at all — the same reason `isShareRefusal` and
|
|
2965
|
+
* `isLeaseRefusal` exist.
|
|
2966
|
+
*/
|
|
2967
|
+
function isPeerAuthRefusal(outcome) {
|
|
2968
|
+
return !outcome.ok;
|
|
2969
|
+
}
|
|
2970
|
+
async function authenticatePeerRequest(headers) {
|
|
2971
|
+
const token = extractShareToken(headers);
|
|
2972
|
+
if (!token) {
|
|
2973
|
+
return {
|
|
2974
|
+
ok: false,
|
|
2975
|
+
body: buildShareRefusal("missing_token", { status: 401 }).body,
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
const grant = await resolveShareToken(token);
|
|
2979
|
+
if (!grant) {
|
|
2980
|
+
return {
|
|
2981
|
+
ok: false,
|
|
2982
|
+
body: buildShareRefusal("unknown_token", { status: 401 }).body,
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
return { ok: true, grant };
|
|
2986
|
+
}
|
|
2987
|
+
/** The model an upstream JSON response says it served, when it says so. */
|
|
2988
|
+
function servedModelName(payload) {
|
|
2989
|
+
if (!payload || typeof payload !== "object") {
|
|
2990
|
+
return undefined;
|
|
2991
|
+
}
|
|
2992
|
+
const model = payload.model;
|
|
2993
|
+
return typeof model === "string" ? model : undefined;
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Soonest window reset across the pool, in seconds.
|
|
2997
|
+
*
|
|
2998
|
+
* A withheld borrower's honest answer to "when should I come back" is when the
|
|
2999
|
+
* lender's tightest window turns over — anything sooner is a guess that invites
|
|
3000
|
+
* a retry storm.
|
|
3001
|
+
*/
|
|
3002
|
+
function earliestShareRecoverySeconds(views, now) {
|
|
3003
|
+
const resets = views
|
|
3004
|
+
.flatMap((view) => [view.sessionResetAt, view.weeklyResetAt])
|
|
3005
|
+
.filter((value) => value !== null && value > now);
|
|
3006
|
+
if (resets.length === 0) {
|
|
3007
|
+
return undefined;
|
|
3008
|
+
}
|
|
3009
|
+
return Math.max(1, Math.round((Math.min(...resets) - now) / 1000));
|
|
3010
|
+
}
|
|
2460
3011
|
async function loadClaudeProxyAccounts(args) {
|
|
2461
3012
|
const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, setRoutingDecision, } = args;
|
|
2462
3013
|
const fs = await import("fs");
|
|
@@ -2646,9 +3197,54 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
2646
3197
|
reconcileEligibleAccountRuntimeState(account);
|
|
2647
3198
|
}
|
|
2648
3199
|
await seedRuntimeQuotasFromDisk(accounts);
|
|
2649
|
-
const
|
|
3200
|
+
const eligibleAccounts = accounts.filter((account) => {
|
|
2650
3201
|
return !getOrCreateRuntimeState(account.key).permanentlyDisabled;
|
|
2651
3202
|
});
|
|
3203
|
+
// Peer-sharing account gates. A borrowed request may only draw on accounts
|
|
3204
|
+
// its grant allows, that still leave the lender's reserved headroom intact,
|
|
3205
|
+
// and whose window slice the grant has not already spent. These are account
|
|
3206
|
+
// properties rather than request properties, which is why they filter the
|
|
3207
|
+
// pool here instead of refusing at the inbound gate.
|
|
3208
|
+
const leaseExpiryFiltered = await dropExpiredResidentAccounts(eligibleAccounts);
|
|
3209
|
+
const leaseScopeFiltered = await dropLeaseDisallowedAccounts(leaseExpiryFiltered.accounts, typeof body.model === "string" ? body.model : undefined);
|
|
3210
|
+
const leaseFiltered = {
|
|
3211
|
+
accounts: leaseScopeFiltered.accounts,
|
|
3212
|
+
withheld: [...leaseExpiryFiltered.withheld, ...leaseScopeFiltered.withheld],
|
|
3213
|
+
};
|
|
3214
|
+
const leasedAccounts = leaseFiltered.accounts;
|
|
3215
|
+
if (leasedAccounts.length === 0 && leaseFiltered.withheld.length > 0) {
|
|
3216
|
+
// Every account here belongs to a lender whose lease has lapsed. The
|
|
3217
|
+
// re-authentication message below would be actively wrong — it would send
|
|
3218
|
+
// the borrower to OAuth into somebody else's account, which cannot work and
|
|
3219
|
+
// should not be attempted. Say what actually happened instead.
|
|
3220
|
+
const detail = leaseFiltered.withheld
|
|
3221
|
+
.map((entry) => `${entry.label} (${entry.reason})`)
|
|
3222
|
+
.join(", ");
|
|
3223
|
+
// Scope and lifetime need different advice. Telling someone to re-sync when
|
|
3224
|
+
// the lender simply never lent them this model sends them in circles.
|
|
3225
|
+
const scopeOnly = leaseFiltered.withheld.every((entry) => entry.reason === "model_not_allowed");
|
|
3226
|
+
const leaseMessage = scopeOnly
|
|
3227
|
+
? `Borrowed account(s) do not cover the requested model: ${detail}. ` +
|
|
3228
|
+
`Ask the lender to widen the share, or use a model it allows.`
|
|
3229
|
+
: `Borrowed account(s) are no longer covered by a lease: ${detail}. ` +
|
|
3230
|
+
`Run 'neurolink proxy peer sync' to check in with the lender, or ask them to resume the share.`;
|
|
3231
|
+
tracer?.setError("permission_error", leaseMessage);
|
|
3232
|
+
tracer?.end(403, Date.now() - requestStartTime);
|
|
3233
|
+
return {
|
|
3234
|
+
response: buildLoggedClaudeError(403, leaseMessage, "permission_error"),
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
const shareFiltered = await applyShareAccountGates({
|
|
3238
|
+
accounts: leasedAccounts,
|
|
3239
|
+
ctx,
|
|
3240
|
+
buildLoggedClaudeError,
|
|
3241
|
+
});
|
|
3242
|
+
if (shareFiltered.refusal) {
|
|
3243
|
+
tracer?.setError("rate_limit_error", shareFiltered.refusal.message);
|
|
3244
|
+
tracer?.end(shareFiltered.refusal.status, Date.now() - requestStartTime);
|
|
3245
|
+
return { response: shareFiltered.refusal.response };
|
|
3246
|
+
}
|
|
3247
|
+
const enabledAccounts = shareFiltered.accounts;
|
|
2652
3248
|
if (enabledAccounts.length === 0) {
|
|
2653
3249
|
const reauthMsg = formatReauthMessage(accounts.map((account) => account.label));
|
|
2654
3250
|
tracer?.setError("authentication_error", reauthMsg);
|
|
@@ -2834,6 +3430,62 @@ async function executeClaudeFallbackWithRetry(args) {
|
|
|
2834
3430
|
}
|
|
2835
3431
|
throw lastError;
|
|
2836
3432
|
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Try each borrowable peer in priority order once the local pool is spent.
|
|
3435
|
+
*
|
|
3436
|
+
* Returns a `Response` for a stream — which must keep streaming — the parsed
|
|
3437
|
+
* JSON body otherwise, or `null` when no peer served, leaving the provider
|
|
3438
|
+
* fallback chain to take over. The two success shapes are what the rest of this
|
|
3439
|
+
* module returns as well; the route adapter tells them apart.
|
|
3440
|
+
*
|
|
3441
|
+
* Deliberately one attempt per peer: this path only runs after every local
|
|
3442
|
+
* account has already been tried, so the request has spent most of its latency
|
|
3443
|
+
* budget. Re-trying a peer that just declined would spend the rest of it.
|
|
3444
|
+
*
|
|
3445
|
+
* A borrowed request is never itself forwarded to a peer. Chaining a lend onto
|
|
3446
|
+
* a lend would spend a third party's capacity under a grant that says nothing
|
|
3447
|
+
* about them, and a cycle between two nodes would bounce a single request
|
|
3448
|
+
* between them until something timed out.
|
|
3449
|
+
*/
|
|
3450
|
+
async function tryBorrowFromPeers(args) {
|
|
3451
|
+
if (getShareContext()) {
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
const peers = await selectBorrowablePeers();
|
|
3455
|
+
if (peers.length === 0) {
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
const stream = args.body.stream === true;
|
|
3459
|
+
// Re-serialize the request as the client shaped it. The peer runs its own
|
|
3460
|
+
// routing and its own pool, so forwarding our resolved account or attempt
|
|
3461
|
+
// state would mean nothing to it.
|
|
3462
|
+
const forwardBody = JSON.stringify(args.body);
|
|
3463
|
+
for (const peer of peers) {
|
|
3464
|
+
logger.always(`[proxy] local pool spent — trying peer=${peer.name}`);
|
|
3465
|
+
const attempt = await forwardToPeer({ peer, body: forwardBody, stream });
|
|
3466
|
+
if (attempt.ok) {
|
|
3467
|
+
logger.always(`[proxy] served by peer=${peer.name}`);
|
|
3468
|
+
args.logFinalRequest(attempt.response.status, `peer:${peer.name}`, "peer");
|
|
3469
|
+
// Hand back what the rest of this module hands back: a locally
|
|
3470
|
+
// constructed Response for a stream, a parsed object for JSON. Returning
|
|
3471
|
+
// the fetch Response itself would be serialized to `{}` by the route
|
|
3472
|
+
// adapter, which only recognizes Responses this process created.
|
|
3473
|
+
if (stream) {
|
|
3474
|
+
return new Response(attempt.response.body, {
|
|
3475
|
+
status: attempt.response.status,
|
|
3476
|
+
headers: {
|
|
3477
|
+
"content-type": attempt.response.headers.get("content-type") ??
|
|
3478
|
+
"text/event-stream",
|
|
3479
|
+
"cache-control": "no-cache",
|
|
3480
|
+
connection: "keep-alive",
|
|
3481
|
+
},
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
return (await attempt.response.json().catch(() => null)) ?? null;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
2837
3489
|
async function tryConfiguredClaudeFallbackChain(args) {
|
|
2838
3490
|
const { ctx, body, parsedFallbackRequest, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
2839
3491
|
const chain = modelRouter?.getFallbackChain() ?? [];
|
|
@@ -3232,6 +3884,10 @@ async function handleAnthropicSuccessfulResponse(args) {
|
|
|
3232
3884
|
logger.always(`[proxy] ← ${response.status} account=${account.label}`);
|
|
3233
3885
|
const quota = parseQuotaHeaders(response.headers, { model: body.model });
|
|
3234
3886
|
if (quota) {
|
|
3887
|
+
// Attribute the window movement to the borrowing grant before the snapshot
|
|
3888
|
+
// is overwritten — this is the only moment both the previous and the new
|
|
3889
|
+
// utilization are in hand, and a slice ceiling is meaningless without it.
|
|
3890
|
+
recordBorrowedWindowDelta(account.key, accountState.quota, quota);
|
|
3235
3891
|
// Stash the latest quota on runtime state so the next request can pick the
|
|
3236
3892
|
// account whose window resets soonest (max-utilization) and proactively
|
|
3237
3893
|
// skip rejected windows unless Anthropic explicitly permits overage.
|
|
@@ -3566,6 +4222,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
3566
4222
|
const capturedResponse = response;
|
|
3567
4223
|
const capturedRequestBytes = finalBodyStr.length;
|
|
3568
4224
|
const capturedAccountLabel = account.label;
|
|
4225
|
+
const capturedAccountKey = account.key;
|
|
3569
4226
|
telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
|
|
3570
4227
|
.then(([data, clientBody, rawOutcome]) => {
|
|
3571
4228
|
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
|
|
@@ -3576,6 +4233,20 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
3576
4233
|
cacheCreationTokens: data.usage.cacheCreationInputTokens,
|
|
3577
4234
|
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
3578
4235
|
});
|
|
4236
|
+
// Bill the borrowing grant from the same totals. A stream's usage is
|
|
4237
|
+
// only final at message_delta, which is exactly here.
|
|
4238
|
+
settleBorrowedRequest(capturedAccountKey, data.model, {
|
|
4239
|
+
inputTokens: data.usage.inputTokens,
|
|
4240
|
+
outputTokens: data.usage.outputTokens,
|
|
4241
|
+
cacheCreationTokens: data.usage.cacheCreationInputTokens,
|
|
4242
|
+
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
4243
|
+
});
|
|
4244
|
+
recordLeasedAccountSpend(capturedAccountLabel, data.model, {
|
|
4245
|
+
inputTokens: data.usage.inputTokens,
|
|
4246
|
+
outputTokens: data.usage.outputTokens,
|
|
4247
|
+
cacheCreationTokens: data.usage.cacheCreationInputTokens,
|
|
4248
|
+
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
4249
|
+
});
|
|
3579
4250
|
capturedTracer.logStreamEvents(data.events);
|
|
3580
4251
|
capturedTracer.setResponseInfo(responseInfoFromStream(data));
|
|
3581
4252
|
const rateLimit5h = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-5h-utilization") ?? "");
|
|
@@ -3676,6 +4347,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
3676
4347
|
});
|
|
3677
4348
|
streamSource = streamSource.pipeThrough(noTracerInterceptor);
|
|
3678
4349
|
const capturedAccountLabel = account.label;
|
|
4350
|
+
const capturedAccountKey = account.key;
|
|
3679
4351
|
telemetryDone = Promise.all([
|
|
3680
4352
|
noTracerTelemetry,
|
|
3681
4353
|
clientCapture,
|
|
@@ -3691,6 +4363,10 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
3691
4363
|
cacheCreationTokens: data.usage.cacheCreationInputTokens,
|
|
3692
4364
|
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
3693
4365
|
};
|
|
4366
|
+
// Settled on the untraced path too: whether telemetry is exported has
|
|
4367
|
+
// nothing to do with whether a borrower should be charged.
|
|
4368
|
+
settleBorrowedRequest(capturedAccountKey, data.model, usage);
|
|
4369
|
+
recordLeasedAccountSpend(capturedAccountLabel, data.model, usage);
|
|
3694
4370
|
if (failure) {
|
|
3695
4371
|
logFinalRequest(failure.status, capturedAccountLabel, account.type, failure.errorType, failure.message, usage);
|
|
3696
4372
|
}
|
|
@@ -3813,6 +4489,10 @@ async function handleAnthropicJsonSuccessResponse(args) {
|
|
|
3813
4489
|
durationMs: Date.now() - requestStartTime,
|
|
3814
4490
|
});
|
|
3815
4491
|
const responseJson = JSON.parse(responseText);
|
|
4492
|
+
// Settlement is not diagnostics. It ran inside the tracer branch, so a node
|
|
4493
|
+
// with tracing off served every borrowed request for free and the lender's
|
|
4494
|
+
// ledger never moved — settle from the response itself instead.
|
|
4495
|
+
settleFromResponseUsage(account, responseJson);
|
|
3816
4496
|
if (tracer && responseJson && typeof responseJson === "object") {
|
|
3817
4497
|
const usage = responseJson.usage;
|
|
3818
4498
|
if (usage) {
|
|
@@ -3928,6 +4608,9 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
|
|
|
3928
4608
|
durationMs: Date.now() - requestStartTime,
|
|
3929
4609
|
});
|
|
3930
4610
|
const retryJson = JSON.parse(retryText);
|
|
4611
|
+
// A response served after an auth retry is a served response: it costs the
|
|
4612
|
+
// lender's account exactly what any other one does.
|
|
4613
|
+
settleFromResponseUsage(account, retryJson);
|
|
3931
4614
|
if (tracer && retryJson && typeof retryJson === "object") {
|
|
3932
4615
|
const retryUsage = retryJson.usage;
|
|
3933
4616
|
if (retryUsage) {
|
|
@@ -5197,6 +5880,14 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
5197
5880
|
setRoutingDecision,
|
|
5198
5881
|
});
|
|
5199
5882
|
if ("response" in loadedAccounts) {
|
|
5883
|
+
// No usable local account. A node that has none of its own — or whose only
|
|
5884
|
+
// accounts are disabled — is still entitled to borrow: that is the whole
|
|
5885
|
+
// point of being lent capacity. Peers are tried before the credentials
|
|
5886
|
+
// error is returned, and the error stands if none of them serves.
|
|
5887
|
+
const peerOnlyResult = await tryBorrowFromPeers({ body, logFinalRequest });
|
|
5888
|
+
if (peerOnlyResult) {
|
|
5889
|
+
return peerOnlyResult;
|
|
5890
|
+
}
|
|
5200
5891
|
return loadedAccounts.response;
|
|
5201
5892
|
}
|
|
5202
5893
|
const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, } = loadedAccounts;
|
|
@@ -5646,6 +6337,13 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
5646
6337
|
// rather than delaying it or returning unrelated fallback output.
|
|
5647
6338
|
if (shouldAttemptClaudeFallback(loopState)) {
|
|
5648
6339
|
let fallbackFailureMessage;
|
|
6340
|
+
// Peers first. A peer serves the same models over the same wire format, so
|
|
6341
|
+
// borrowing costs one extra hop, while the provider chain below has to
|
|
6342
|
+
// reshape the request for a different API and answers as a different model.
|
|
6343
|
+
const peerResult = await tryBorrowFromPeers({ body, logFinalRequest });
|
|
6344
|
+
if (peerResult) {
|
|
6345
|
+
return peerResult;
|
|
6346
|
+
}
|
|
5649
6347
|
const configuredFallbackResult = await tryConfiguredClaudeFallbackChain({
|
|
5650
6348
|
ctx,
|
|
5651
6349
|
body,
|
|
@@ -5948,6 +6646,358 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
5948
6646
|
streaming: { enabled: true, contentType: "text/event-stream" },
|
|
5949
6647
|
},
|
|
5950
6648
|
// =====================================================================
|
|
6649
|
+
// POST /peer/heartbeat -- complete-mode check-in
|
|
6650
|
+
//
|
|
6651
|
+
// The only control surface a complete-mode borrower still touches. It
|
|
6652
|
+
// answers with a fresh lease while the grant is active, and with a stop
|
|
6653
|
+
// once it is not — which is how a pause reaches a borrower whose requests
|
|
6654
|
+
// never come through here at all.
|
|
6655
|
+
// =====================================================================
|
|
6656
|
+
{
|
|
6657
|
+
method: "POST",
|
|
6658
|
+
path: `${basePath}/peer/heartbeat`,
|
|
6659
|
+
handler: async (ctx) => {
|
|
6660
|
+
const token = ctx.headers["x-neurolink-share-token"];
|
|
6661
|
+
const grantId = ctx.headers["x-neurolink-grant-id"];
|
|
6662
|
+
if (!token || !grantId) {
|
|
6663
|
+
// A stop, not an HTTP error: the borrower reads `ok`/`stop` from
|
|
6664
|
+
// the body, and a status code here would be discarded by the route
|
|
6665
|
+
// adaptor while reading as though it were enforced.
|
|
6666
|
+
return { ok: false, stop: true, reason: "no grant identified" };
|
|
6667
|
+
}
|
|
6668
|
+
const grant = await getShareGrant(grantId);
|
|
6669
|
+
// The lease secret is the shared credential for this surface: the
|
|
6670
|
+
// borrower proves identity with the same secret it verifies leases
|
|
6671
|
+
// with, so a heartbeat needs no separate token to leak.
|
|
6672
|
+
if (!grant ||
|
|
6673
|
+
!grant.leaseSecret ||
|
|
6674
|
+
!secretsMatch(grant.leaseSecret, token)) {
|
|
6675
|
+
return { ok: false, stop: true, reason: "grant not recognized" };
|
|
6676
|
+
}
|
|
6677
|
+
if (grant.level !== "complete") {
|
|
6678
|
+
return {
|
|
6679
|
+
ok: false,
|
|
6680
|
+
stop: true,
|
|
6681
|
+
reason: "grant is not a complete share",
|
|
6682
|
+
};
|
|
6683
|
+
}
|
|
6684
|
+
if (grant.state !== "active") {
|
|
6685
|
+
return { ok: false, stop: true, reason: grant.state };
|
|
6686
|
+
}
|
|
6687
|
+
// Fold in what the borrower says it spent. This is self-reported and
|
|
6688
|
+
// treated as such — `share status` reconciles it against the
|
|
6689
|
+
// account's real utilization, which the borrower cannot influence.
|
|
6690
|
+
const body = ctx.body;
|
|
6691
|
+
const reported = Number(body?.coinsSpent ?? 0);
|
|
6692
|
+
if (Number.isFinite(reported) && reported > 0) {
|
|
6693
|
+
await recordReportedResidentSpend(grant, reported);
|
|
6694
|
+
}
|
|
6695
|
+
// Weigh the claim against what the account actually did. A borrower
|
|
6696
|
+
// that stops reporting is invisible in every other signal we have.
|
|
6697
|
+
const drift = await auditCompleteShareHeartbeat(grant, Number.isFinite(reported) ? reported : 0);
|
|
6698
|
+
if (drift.paused) {
|
|
6699
|
+
logger.always(`[proxy] auto-paused share ${grant.peerLabel}: ${drift.detail}`);
|
|
6700
|
+
return { ok: false, stop: true, reason: "usage drift" };
|
|
6701
|
+
}
|
|
6702
|
+
return { ok: true, lease: issueLease(grant) };
|
|
6703
|
+
},
|
|
6704
|
+
description: "Complete-share heartbeat: report spend, renew the lease",
|
|
6705
|
+
tags: ["claude-proxy", "sharing"],
|
|
6706
|
+
},
|
|
6707
|
+
// =====================================================================
|
|
6708
|
+
// GET /peer/handshake -- version and capability negotiation
|
|
6709
|
+
//
|
|
6710
|
+
// The cheapest possible "are we still on speaking terms": it touches no
|
|
6711
|
+
// account, spends no capacity and reaches no upstream, so a borrower can
|
|
6712
|
+
// call it on a timer. It reports the grant's own state and nothing about
|
|
6713
|
+
// the pool behind it.
|
|
6714
|
+
// =====================================================================
|
|
6715
|
+
{
|
|
6716
|
+
method: "GET",
|
|
6717
|
+
path: `${basePath}/peer/handshake`,
|
|
6718
|
+
handler: async (ctx) => {
|
|
6719
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6720
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6721
|
+
return auth.body;
|
|
6722
|
+
}
|
|
6723
|
+
return {
|
|
6724
|
+
ok: true,
|
|
6725
|
+
protocol: PEER_PROTOCOL_VERSION,
|
|
6726
|
+
capabilities: PEER_CAPABILITIES,
|
|
6727
|
+
grant: {
|
|
6728
|
+
peerLabel: auth.grant.peerLabel,
|
|
6729
|
+
level: auth.grant.level,
|
|
6730
|
+
state: auth.grant.state,
|
|
6731
|
+
ledger: auth.grant.entitlement.ledger,
|
|
6732
|
+
},
|
|
6733
|
+
};
|
|
6734
|
+
},
|
|
6735
|
+
description: "Peer handshake: protocol version, capabilities, grant state",
|
|
6736
|
+
tags: ["claude-proxy", "sharing"],
|
|
6737
|
+
},
|
|
6738
|
+
// =====================================================================
|
|
6739
|
+
// POST /peer/provision -- borrower lodges a PKCE challenge
|
|
6740
|
+
//
|
|
6741
|
+
// Split provisioning: the borrower keeps the verifier and sends only its
|
|
6742
|
+
// digest, so the lender is never in possession of anything that could
|
|
6743
|
+
// become a credential. The lender authorizes in its own browser and
|
|
6744
|
+
// relays back a code that is useless without the verifier.
|
|
6745
|
+
// =====================================================================
|
|
6746
|
+
{
|
|
6747
|
+
method: "POST",
|
|
6748
|
+
path: `${basePath}/peer/provision`,
|
|
6749
|
+
handler: async (ctx) => {
|
|
6750
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6751
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6752
|
+
return auth.body;
|
|
6753
|
+
}
|
|
6754
|
+
if (auth.grant.state !== "active") {
|
|
6755
|
+
return buildShareRefusal(auth.grant.state === "paused" ? "paused" : "revoked", { status: 403, grant: auth.grant }).body;
|
|
6756
|
+
}
|
|
6757
|
+
if (auth.grant.level !== "complete") {
|
|
6758
|
+
return {
|
|
6759
|
+
type: "error",
|
|
6760
|
+
error: {
|
|
6761
|
+
type: "invalid_request_error",
|
|
6762
|
+
message: "This is a live share. Ask the lender to run " +
|
|
6763
|
+
"`neurolink proxy share level --to complete` first.",
|
|
6764
|
+
},
|
|
6765
|
+
};
|
|
6766
|
+
}
|
|
6767
|
+
const body = ctx.body;
|
|
6768
|
+
const opened = await openProvisionRequest({
|
|
6769
|
+
grantId: auth.grant.id,
|
|
6770
|
+
codeChallenge: String(body?.codeChallenge ?? ""),
|
|
6771
|
+
state: String(body?.state ?? ""),
|
|
6772
|
+
});
|
|
6773
|
+
if (opened.ok !== true) {
|
|
6774
|
+
return {
|
|
6775
|
+
type: "error",
|
|
6776
|
+
error: {
|
|
6777
|
+
type: "invalid_request_error",
|
|
6778
|
+
message: opened.reason,
|
|
6779
|
+
},
|
|
6780
|
+
};
|
|
6781
|
+
}
|
|
6782
|
+
logger.always(`[proxy] ${auth.grant.peerLabel} asked to be provisioned — run ` +
|
|
6783
|
+
`\`neurolink proxy share provision --peer ${auth.grant.peerLabel}\``);
|
|
6784
|
+
return {
|
|
6785
|
+
ok: true,
|
|
6786
|
+
status: "pending",
|
|
6787
|
+
expiresAt: opened.request.expiresAt,
|
|
6788
|
+
};
|
|
6789
|
+
},
|
|
6790
|
+
description: "Lodge a PKCE challenge for a resident credential (complete shares)",
|
|
6791
|
+
tags: ["claude-proxy", "sharing"],
|
|
6792
|
+
},
|
|
6793
|
+
// =====================================================================
|
|
6794
|
+
// GET /peer/provision -- borrower collects its authorization code
|
|
6795
|
+
//
|
|
6796
|
+
// Answers once. A code that could be claimed twice would let a replay
|
|
6797
|
+
// mint a second credential on the lender's account, so consumption is
|
|
6798
|
+
// recorded before the value is handed over.
|
|
6799
|
+
// =====================================================================
|
|
6800
|
+
{
|
|
6801
|
+
method: "GET",
|
|
6802
|
+
path: `${basePath}/peer/provision`,
|
|
6803
|
+
handler: async (ctx) => {
|
|
6804
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6805
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6806
|
+
return auth.body;
|
|
6807
|
+
}
|
|
6808
|
+
const claimed = await claimProvisionRequest(auth.grant.id);
|
|
6809
|
+
if (claimed.status !== "ready") {
|
|
6810
|
+
return { ok: true, status: claimed.status };
|
|
6811
|
+
}
|
|
6812
|
+
// The heartbeat address the borrower will call home on. Without it a
|
|
6813
|
+
// resident grant can never renew and stops at its offline grace.
|
|
6814
|
+
const lenderUrl = await getNodePublicUrl();
|
|
6815
|
+
return {
|
|
6816
|
+
ok: true,
|
|
6817
|
+
status: "ready",
|
|
6818
|
+
claim: buildProvisionClaim({
|
|
6819
|
+
grant: auth.grant,
|
|
6820
|
+
// The borrower renames this locally at `peer request --name`;
|
|
6821
|
+
// here it only has to make the token-store label unique.
|
|
6822
|
+
lenderName: "lender",
|
|
6823
|
+
...(lenderUrl ? { lenderUrl } : {}),
|
|
6824
|
+
code: claimed.code,
|
|
6825
|
+
state: claimed.state,
|
|
6826
|
+
}),
|
|
6827
|
+
};
|
|
6828
|
+
},
|
|
6829
|
+
description: "Collect the authorization code the lender produced",
|
|
6830
|
+
tags: ["claude-proxy", "sharing"],
|
|
6831
|
+
},
|
|
6832
|
+
// =====================================================================
|
|
6833
|
+
// GET /peer/receipts -- collect signed statements of what was charged
|
|
6834
|
+
//
|
|
6835
|
+
// So the lender's word is not the only record. Each receipt carries the
|
|
6836
|
+
// usage it was computed from, and sequences are contiguous, so a borrower
|
|
6837
|
+
// can recompute every charge and see a withheld one as a gap.
|
|
6838
|
+
// =====================================================================
|
|
6839
|
+
{
|
|
6840
|
+
method: "GET",
|
|
6841
|
+
path: `${basePath}/peer/receipts`,
|
|
6842
|
+
handler: async (ctx) => {
|
|
6843
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6844
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6845
|
+
return auth.body;
|
|
6846
|
+
}
|
|
6847
|
+
const since = Number(ctx.query?.since ?? 0);
|
|
6848
|
+
const collected = await listShareReceipts(auth.grant.id, Number.isFinite(since) ? since : 0);
|
|
6849
|
+
return { ok: true, receipts: collected };
|
|
6850
|
+
},
|
|
6851
|
+
description: "Collect signed receipts for this grant's settled charges",
|
|
6852
|
+
tags: ["claude-proxy", "sharing"],
|
|
6853
|
+
},
|
|
6854
|
+
// =====================================================================
|
|
6855
|
+
// POST /peer/net -- settle one round of reciprocal netting
|
|
6856
|
+
//
|
|
6857
|
+
// Both sides state cumulative positions, and the round forgives the
|
|
6858
|
+
// overlap not yet forgiven. Cumulative rather than incremental is what
|
|
6859
|
+
// makes a replayed round free rather than a second payout.
|
|
6860
|
+
// =====================================================================
|
|
6861
|
+
{
|
|
6862
|
+
method: "POST",
|
|
6863
|
+
path: `${basePath}/peer/net`,
|
|
6864
|
+
handler: async (ctx) => {
|
|
6865
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6866
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6867
|
+
return auth.body;
|
|
6868
|
+
}
|
|
6869
|
+
const secret = auth.grant.receiptSecret;
|
|
6870
|
+
if (!secret) {
|
|
6871
|
+
return {
|
|
6872
|
+
type: "error",
|
|
6873
|
+
error: {
|
|
6874
|
+
type: "invalid_request_error",
|
|
6875
|
+
message: "This grant predates receipts and cannot be netted. " +
|
|
6876
|
+
"Ask the lender to re-issue it.",
|
|
6877
|
+
},
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
const body = ctx.body;
|
|
6881
|
+
const consumedByYou = Number(body?.consumedByYou ?? NaN);
|
|
6882
|
+
const alreadyNetted = Number(body?.alreadyNetted ?? NaN);
|
|
6883
|
+
if (!Number.isFinite(consumedByYou) ||
|
|
6884
|
+
!Number.isFinite(alreadyNetted)) {
|
|
6885
|
+
return {
|
|
6886
|
+
type: "error",
|
|
6887
|
+
error: {
|
|
6888
|
+
type: "invalid_request_error",
|
|
6889
|
+
message: "A netting claim needs both cumulative totals.",
|
|
6890
|
+
},
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6893
|
+
// The claim is the peer's own accounting, so it is signed: a figure
|
|
6894
|
+
// that credits the caller must not be forgeable by anyone who merely
|
|
6895
|
+
// reaches the port.
|
|
6896
|
+
const authentic = verifySharePayload({ consumedByYou, alreadyNetted, grantId: auth.grant.id }, String(body?.signature ?? ""), secret);
|
|
6897
|
+
if (!authentic) {
|
|
6898
|
+
return buildShareRefusal("unknown_token", { status: 401 }).body;
|
|
6899
|
+
}
|
|
6900
|
+
const result = await applyReciprocalNetting({
|
|
6901
|
+
grantId: auth.grant.id,
|
|
6902
|
+
consumedFromPeer: consumedByYou,
|
|
6903
|
+
peerAlreadyNetted: alreadyNetted,
|
|
6904
|
+
});
|
|
6905
|
+
return { ok: true, ...result };
|
|
6906
|
+
},
|
|
6907
|
+
description: "Settle one round of reciprocal netting against this grant",
|
|
6908
|
+
tags: ["claude-proxy", "sharing"],
|
|
6909
|
+
},
|
|
6910
|
+
// =====================================================================
|
|
6911
|
+
// POST /peer/note -- check or redeem a transferable coin note
|
|
6912
|
+
//
|
|
6913
|
+
// Holding the note is the credential for a check; redeeming additionally
|
|
6914
|
+
// needs a grant to credit. Marking spent and crediting happen under one
|
|
6915
|
+
// lock, so two holders racing the same note produce exactly one credit.
|
|
6916
|
+
// =====================================================================
|
|
6917
|
+
{
|
|
6918
|
+
method: "POST",
|
|
6919
|
+
path: `${basePath}/peer/note`,
|
|
6920
|
+
handler: async (ctx) => {
|
|
6921
|
+
const body = ctx.body;
|
|
6922
|
+
const note = decodeShareNote(String(body?.note ?? ""));
|
|
6923
|
+
if (!note) {
|
|
6924
|
+
return {
|
|
6925
|
+
type: "error",
|
|
6926
|
+
error: {
|
|
6927
|
+
type: "invalid_request_error",
|
|
6928
|
+
message: "That is not a NeuroLink coin note.",
|
|
6929
|
+
},
|
|
6930
|
+
};
|
|
6931
|
+
}
|
|
6932
|
+
const secret = await getNoteSecret();
|
|
6933
|
+
if (!body?.redeem) {
|
|
6934
|
+
// A status check needs no grant: the note itself is the credential,
|
|
6935
|
+
// and the answer tells a stranger nothing they did not already hold.
|
|
6936
|
+
const inspected = await inspectShareNote(note, secret);
|
|
6937
|
+
return { ok: true, ...inspected };
|
|
6938
|
+
}
|
|
6939
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6940
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6941
|
+
return auth.body;
|
|
6942
|
+
}
|
|
6943
|
+
if (auth.grant.entitlement.ledger !== "coins") {
|
|
6944
|
+
return {
|
|
6945
|
+
type: "error",
|
|
6946
|
+
error: {
|
|
6947
|
+
type: "invalid_request_error",
|
|
6948
|
+
message: "This grant is unlimited — there is no balance to redeem into.",
|
|
6949
|
+
},
|
|
6950
|
+
};
|
|
6951
|
+
}
|
|
6952
|
+
const redeemed = await redeemShareNote({
|
|
6953
|
+
note,
|
|
6954
|
+
grantId: auth.grant.id,
|
|
6955
|
+
secret,
|
|
6956
|
+
});
|
|
6957
|
+
if (redeemed.ok !== true) {
|
|
6958
|
+
return {
|
|
6959
|
+
ok: false,
|
|
6960
|
+
status: redeemed.status,
|
|
6961
|
+
error: {
|
|
6962
|
+
type: "invalid_request_error",
|
|
6963
|
+
message: `That note is ${redeemed.status}.`,
|
|
6964
|
+
},
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
return {
|
|
6968
|
+
ok: true,
|
|
6969
|
+
status: "redeemed",
|
|
6970
|
+
coins: redeemed.coins,
|
|
6971
|
+
balance: redeemed.balance ?? null,
|
|
6972
|
+
};
|
|
6973
|
+
},
|
|
6974
|
+
description: "Check or redeem a transferable coin note",
|
|
6975
|
+
tags: ["claude-proxy", "sharing"],
|
|
6976
|
+
},
|
|
6977
|
+
// =====================================================================
|
|
6978
|
+
// GET /peer/limits -- what this grant may still do
|
|
6979
|
+
//
|
|
6980
|
+
// So a borrower can decide *before* spending a request whether this peer
|
|
6981
|
+
// is worth the extra hop. Everything here is scoped to the caller's own
|
|
6982
|
+
// grant: no account labels, no per-account figures, nothing that would
|
|
6983
|
+
// describe the lender's pool.
|
|
6984
|
+
// =====================================================================
|
|
6985
|
+
{
|
|
6986
|
+
method: "GET",
|
|
6987
|
+
path: `${basePath}/peer/limits`,
|
|
6988
|
+
handler: async (ctx) => {
|
|
6989
|
+
const auth = await authenticatePeerRequest(ctx.headers);
|
|
6990
|
+
if (isPeerAuthRefusal(auth)) {
|
|
6991
|
+
return auth.body;
|
|
6992
|
+
}
|
|
6993
|
+
const limitsRouting = runtimeConfigProvider?.();
|
|
6994
|
+
const snapshot = await buildPeerLimitsSnapshot(auth.grant, limitsRouting?.accountAllowlist ?? accountAllowlist);
|
|
6995
|
+
return { ok: true, ...snapshot };
|
|
6996
|
+
},
|
|
6997
|
+
description: "Peer limits: remaining coins, slice left, whether the grant can be served",
|
|
6998
|
+
tags: ["claude-proxy", "sharing"],
|
|
6999
|
+
},
|
|
7000
|
+
// =====================================================================
|
|
5951
7001
|
// GET /v1/models -- List available models (Anthropic schema)
|
|
5952
7002
|
//
|
|
5953
7003
|
// Returns the Anthropic-shaped list response (`type`, `display_name`,
|
|
@@ -6011,6 +7061,19 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
6011
7061
|
method: "GET",
|
|
6012
7062
|
path: `${basePath}/limits`,
|
|
6013
7063
|
handler: async (ctx) => {
|
|
7064
|
+
// `/limits` names every account and its quota — for an OAuth account
|
|
7065
|
+
// the label is the operator's email. That is an operator diagnostic,
|
|
7066
|
+
// not something a borrower may read, and a refresh also drives real
|
|
7067
|
+
// usage-API calls on the lender's accounts. Borrowed traffic gets
|
|
7068
|
+
// `/peer/limits`, which answers the same routing question about the
|
|
7069
|
+
// borrower's own grant without describing the pool.
|
|
7070
|
+
if (getShareContext()) {
|
|
7071
|
+
return buildShareRefusal("no_capacity", {
|
|
7072
|
+
status: 403,
|
|
7073
|
+
message: "This proxy does not expose pool limits to peers. " +
|
|
7074
|
+
"Use GET /peer/limits for what your grant may still do.",
|
|
7075
|
+
}).body;
|
|
7076
|
+
}
|
|
6014
7077
|
const limitsRouting = runtimeConfigProvider?.();
|
|
6015
7078
|
const effectiveAllowlist = limitsRouting?.accountAllowlist ?? accountAllowlist;
|
|
6016
7079
|
// A refresh reconciles cooldowns from the fetched quota, so it makes
|