@blockrun/clawrouter 0.12.282 → 0.12.284
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/README.md +23 -22
- package/dist/cli.js +281 -35
- package/dist/index.d.ts +14 -0
- package/dist/index.js +239 -28
- package/docs/9-free-ai-models-zero-cost-blockrun.md +5 -5
- package/docs/anthropic-cost-savings.md +1 -1
- package/docs/anthropic-third-party-harness-changes.md +3 -3
- package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +3 -3
- package/docs/configuration.md +6 -0
- package/package.json +2 -2
- package/skills/clawrouter/SKILL.md +3 -3
package/dist/cli.js
CHANGED
|
@@ -32315,6 +32315,11 @@ function getSharedSpendControl() {
|
|
|
32315
32315
|
host.__clawrouterSharedSpendControl ??= new SpendControl();
|
|
32316
32316
|
return host.__clawrouterSharedSpendControl;
|
|
32317
32317
|
}
|
|
32318
|
+
function parseQuotedAmountMicros(selected) {
|
|
32319
|
+
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
32320
|
+
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
|
|
32321
|
+
return BigInt(raw);
|
|
32322
|
+
}
|
|
32318
32323
|
function parseQuotedAmountUsd(selected) {
|
|
32319
32324
|
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
32320
32325
|
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
|
|
@@ -33080,6 +33085,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33080
33085
|
return est === void 0 ? void 0 : Number(est);
|
|
33081
33086
|
};
|
|
33082
33087
|
const needMicros = estimateMicros();
|
|
33088
|
+
const notifyDeclinedPayment = () => {
|
|
33089
|
+
if (!options?.onPaymentDeclined) return;
|
|
33090
|
+
try {
|
|
33091
|
+
options.onPaymentDeclined();
|
|
33092
|
+
} catch (error) {
|
|
33093
|
+
console.error(
|
|
33094
|
+
`[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
|
|
33095
|
+
);
|
|
33096
|
+
}
|
|
33097
|
+
};
|
|
33083
33098
|
const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
|
|
33084
33099
|
const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
|
|
33085
33100
|
if (preAuthCovers) {
|
|
@@ -33098,6 +33113,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33098
33113
|
return response2;
|
|
33099
33114
|
}
|
|
33100
33115
|
paymentInFlight = false;
|
|
33116
|
+
notifyDeclinedPayment();
|
|
33101
33117
|
cache2.delete(cacheKey2);
|
|
33102
33118
|
} catch (err) {
|
|
33103
33119
|
cache2.delete(cacheKey2);
|
|
@@ -33142,6 +33158,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33142
33158
|
clonedRequest.headers.set(key3, value);
|
|
33143
33159
|
}
|
|
33144
33160
|
const paidResponse = await baseFetch(clonedRequest);
|
|
33161
|
+
if (paidResponse.status === 402) notifyDeclinedPayment();
|
|
33145
33162
|
notifyAcceptedPayment(paidResponse, payload);
|
|
33146
33163
|
return paidResponse;
|
|
33147
33164
|
};
|
|
@@ -64789,6 +64806,132 @@ var init_api_key = __esm({
|
|
|
64789
64806
|
}
|
|
64790
64807
|
});
|
|
64791
64808
|
|
|
64809
|
+
// src/session-spend.ts
|
|
64810
|
+
function parseMicros(raw) {
|
|
64811
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
64812
|
+
return BigInt(raw);
|
|
64813
|
+
}
|
|
64814
|
+
function usdToMicros(usd2) {
|
|
64815
|
+
return BigInt(Math.round(usd2 * 1e6));
|
|
64816
|
+
}
|
|
64817
|
+
function microsToUsd(micros) {
|
|
64818
|
+
return Number(micros) / 1e6;
|
|
64819
|
+
}
|
|
64820
|
+
var SessionSpend, CostCapError;
|
|
64821
|
+
var init_session_spend = __esm({
|
|
64822
|
+
"src/session-spend.ts"() {
|
|
64823
|
+
"use strict";
|
|
64824
|
+
init_spend_control();
|
|
64825
|
+
SessionSpend = class _SessionSpend {
|
|
64826
|
+
constructor(store, sessionId, capMicros, holdMicros) {
|
|
64827
|
+
this.store = store;
|
|
64828
|
+
this.sessionId = sessionId;
|
|
64829
|
+
this.capMicros = capMicros;
|
|
64830
|
+
this.heldMicros = holdMicros;
|
|
64831
|
+
if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
|
|
64832
|
+
}
|
|
64833
|
+
store;
|
|
64834
|
+
sessionId;
|
|
64835
|
+
capMicros;
|
|
64836
|
+
/** Budget this request still holds against the session (strict mode only). */
|
|
64837
|
+
heldMicros;
|
|
64838
|
+
/** Sum of payments signed by this request that still count. */
|
|
64839
|
+
signedMicros = 0n;
|
|
64840
|
+
/** The most recent signature not yet settled or voided — the one a decline undoes. */
|
|
64841
|
+
lastSignedMicros = 0n;
|
|
64842
|
+
released = false;
|
|
64843
|
+
/**
|
|
64844
|
+
* Strict mode: admit the request only if the session's spend, plus what its
|
|
64845
|
+
* other in-flight requests hold, plus this request's estimate fits the cap —
|
|
64846
|
+
* and hold the estimate in the same step.
|
|
64847
|
+
*/
|
|
64848
|
+
static admit(store, sessionId, capMicros, estimateMicros) {
|
|
64849
|
+
const spentMicros = store.getSessionCostMicros(sessionId);
|
|
64850
|
+
const heldMicros = store.getSessionHoldMicros(sessionId);
|
|
64851
|
+
if (spentMicros + heldMicros + estimateMicros > capMicros) {
|
|
64852
|
+
return { admitted: false, spentMicros, heldMicros, estimateMicros };
|
|
64853
|
+
}
|
|
64854
|
+
return {
|
|
64855
|
+
admitted: true,
|
|
64856
|
+
spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
|
|
64857
|
+
};
|
|
64858
|
+
}
|
|
64859
|
+
/** Graceful mode: track signed spend, enforce nothing. */
|
|
64860
|
+
static track(store, sessionId) {
|
|
64861
|
+
return new _SessionSpend(store, sessionId, void 0, 0n);
|
|
64862
|
+
}
|
|
64863
|
+
get isStrict() {
|
|
64864
|
+
return this.capMicros !== void 0;
|
|
64865
|
+
}
|
|
64866
|
+
/**
|
|
64867
|
+
* Before a payment is created: may this request sign `quoteMicros`?
|
|
64868
|
+
* Spend already recorded (including this request's own earlier signatures)
|
|
64869
|
+
* plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
|
|
64870
|
+
* Always true in graceful mode.
|
|
64871
|
+
*/
|
|
64872
|
+
coverPayment(quoteMicros) {
|
|
64873
|
+
if (this.capMicros === void 0) return true;
|
|
64874
|
+
const spent = this.store.getSessionCostMicros(this.sessionId);
|
|
64875
|
+
const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
|
|
64876
|
+
if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
|
|
64877
|
+
if (quoteMicros > this.heldMicros) {
|
|
64878
|
+
this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
|
|
64879
|
+
this.heldMicros = quoteMicros;
|
|
64880
|
+
}
|
|
64881
|
+
return true;
|
|
64882
|
+
}
|
|
64883
|
+
/** A payment was signed: it is spend now, and replaces part of the hold. */
|
|
64884
|
+
recordSigned(amountMicros) {
|
|
64885
|
+
if (amountMicros <= 0n) return;
|
|
64886
|
+
this.signedMicros += amountMicros;
|
|
64887
|
+
this.lastSignedMicros = amountMicros;
|
|
64888
|
+
this.store.addSessionCost(this.sessionId, amountMicros);
|
|
64889
|
+
this.shrinkHold(amountMicros);
|
|
64890
|
+
}
|
|
64891
|
+
/**
|
|
64892
|
+
* The gateway answered the payment just signed with a 402: nothing settled.
|
|
64893
|
+
* Payments within one request are signed and sent one at a time, so the
|
|
64894
|
+
* decline always belongs to the latest signature. Undone at most once.
|
|
64895
|
+
*/
|
|
64896
|
+
voidLastSigned() {
|
|
64897
|
+
const undo = this.lastSignedMicros;
|
|
64898
|
+
this.lastSignedMicros = 0n;
|
|
64899
|
+
if (undo <= 0n) return;
|
|
64900
|
+
this.signedMicros -= undo;
|
|
64901
|
+
this.store.addSessionCost(this.sessionId, -undo);
|
|
64902
|
+
}
|
|
64903
|
+
/**
|
|
64904
|
+
* An attempt succeeded. When nothing was signed for this request (the API
|
|
64905
|
+
* key rail), the local estimate is the only record of the spend. When a
|
|
64906
|
+
* payment was signed, it is already recorded at its real amount.
|
|
64907
|
+
*/
|
|
64908
|
+
recordSuccess(estimateMicros) {
|
|
64909
|
+
if (this.signedMicros > 0n || estimateMicros <= 0n) return;
|
|
64910
|
+
this.store.addSessionCost(this.sessionId, estimateMicros);
|
|
64911
|
+
this.shrinkHold(estimateMicros);
|
|
64912
|
+
}
|
|
64913
|
+
/** The request is over: return whatever it still holds. Idempotent. */
|
|
64914
|
+
release() {
|
|
64915
|
+
if (this.released) return;
|
|
64916
|
+
this.released = true;
|
|
64917
|
+
this.shrinkHold(this.heldMicros);
|
|
64918
|
+
}
|
|
64919
|
+
shrinkHold(byMicros) {
|
|
64920
|
+
const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
|
|
64921
|
+
if (drop <= 0n) return;
|
|
64922
|
+
this.heldMicros -= drop;
|
|
64923
|
+
this.store.adjustSessionHold(this.sessionId, -drop);
|
|
64924
|
+
}
|
|
64925
|
+
};
|
|
64926
|
+
CostCapError = class extends SpendPolicyError {
|
|
64927
|
+
constructor(reason) {
|
|
64928
|
+
super(reason);
|
|
64929
|
+
this.name = "CostCapError";
|
|
64930
|
+
}
|
|
64931
|
+
};
|
|
64932
|
+
}
|
|
64933
|
+
});
|
|
64934
|
+
|
|
64792
64935
|
// src/compression/types.ts
|
|
64793
64936
|
var DEFAULT_COMPRESSION_CONFIG;
|
|
64794
64937
|
var init_types2 = __esm({
|
|
@@ -65636,6 +65779,9 @@ var init_session = __esm({
|
|
|
65636
65779
|
};
|
|
65637
65780
|
SessionStore = class {
|
|
65638
65781
|
sessions = /* @__PURE__ */ new Map();
|
|
65782
|
+
// Kept apart from `sessions`: a hold belongs to requests still running, so it
|
|
65783
|
+
// must not vanish when the idle-session sweep drops an entry mid-request.
|
|
65784
|
+
sessionHolds = /* @__PURE__ */ new Map();
|
|
65639
65785
|
config;
|
|
65640
65786
|
cleanupInterval = null;
|
|
65641
65787
|
constructor(config = {}) {
|
|
@@ -65811,14 +65957,34 @@ var init_session = __esm({
|
|
|
65811
65957
|
this.sessions.set(sessionId, entry);
|
|
65812
65958
|
}
|
|
65813
65959
|
entry.sessionCostMicros += additionalMicros;
|
|
65960
|
+
if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
|
|
65814
65961
|
}
|
|
65815
65962
|
/**
|
|
65816
65963
|
* Get the total accumulated cost for a session in USD.
|
|
65817
65964
|
*/
|
|
65818
65965
|
getSessionCostUsd(sessionId) {
|
|
65819
|
-
|
|
65820
|
-
|
|
65821
|
-
|
|
65966
|
+
return Number(this.getSessionCostMicros(sessionId)) / 1e6;
|
|
65967
|
+
}
|
|
65968
|
+
/**
|
|
65969
|
+
* Get the total accumulated cost for a session in USDC micros.
|
|
65970
|
+
*/
|
|
65971
|
+
getSessionCostMicros(sessionId) {
|
|
65972
|
+
return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
|
|
65973
|
+
}
|
|
65974
|
+
/**
|
|
65975
|
+
* Budget held by a session's in-flight requests (USDC micros) — admitted
|
|
65976
|
+
* against the strict cap but not yet spent. See session-spend.ts.
|
|
65977
|
+
*/
|
|
65978
|
+
getSessionHoldMicros(sessionId) {
|
|
65979
|
+
return this.sessionHolds.get(sessionId) ?? 0n;
|
|
65980
|
+
}
|
|
65981
|
+
/**
|
|
65982
|
+
* Grow (positive) or release (negative) a session's in-flight hold.
|
|
65983
|
+
*/
|
|
65984
|
+
adjustSessionHold(sessionId, deltaMicros) {
|
|
65985
|
+
const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
|
|
65986
|
+
if (next > 0n) this.sessionHolds.set(sessionId, next);
|
|
65987
|
+
else this.sessionHolds.delete(sessionId);
|
|
65822
65988
|
}
|
|
65823
65989
|
/**
|
|
65824
65990
|
* Stop the cleanup interval.
|
|
@@ -96521,6 +96687,19 @@ async function startProxy(options) {
|
|
|
96521
96687
|
}
|
|
96522
96688
|
const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
|
|
96523
96689
|
const x402 = authMode === "wallet" ? new x402Client() : void 0;
|
|
96690
|
+
x402?.onBeforePaymentCreation(async (context) => {
|
|
96691
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96692
|
+
if (!spend?.isStrict) return;
|
|
96693
|
+
const quote = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96694
|
+
if (quote === void 0) {
|
|
96695
|
+
throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
|
|
96696
|
+
}
|
|
96697
|
+
if (!spend.coverPayment(quote)) {
|
|
96698
|
+
throw new CostCapError(
|
|
96699
|
+
`ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
|
|
96700
|
+
);
|
|
96701
|
+
}
|
|
96702
|
+
});
|
|
96524
96703
|
if (x402 && account) {
|
|
96525
96704
|
const evmPublicClient = createPublicClient({ chain: base, transport: http() });
|
|
96526
96705
|
const evmSigner = toClientEvmSigner(account, evmPublicClient);
|
|
@@ -96563,6 +96742,11 @@ async function startProxy(options) {
|
|
|
96563
96742
|
if (store) store.amountUsd = amountUsd;
|
|
96564
96743
|
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
96565
96744
|
});
|
|
96745
|
+
x402?.onAfterPaymentCreation(async (context) => {
|
|
96746
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96747
|
+
const amount = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96748
|
+
if (spend && amount !== void 0) spend.recordSigned(amount);
|
|
96749
|
+
});
|
|
96566
96750
|
const payFetch = authMode === "api-key" ? withSpendPolicy(
|
|
96567
96751
|
createApiKeyFetch(apiKey, fetch, apiBase),
|
|
96568
96752
|
options.spendControl ?? getSharedSpendControl(),
|
|
@@ -96581,7 +96765,10 @@ async function startProxy(options) {
|
|
|
96581
96765
|
// Only the wallet rail settles per call. The API-key rail is billed
|
|
96582
96766
|
// server-side against account credit, so there is no per-request
|
|
96583
96767
|
// settlement for an observer to report.
|
|
96584
|
-
onPayment: options.onPayment
|
|
96768
|
+
onPayment: options.onPayment,
|
|
96769
|
+
// A 402 answer to a signed payment means the gateway declined it and
|
|
96770
|
+
// nothing settled — take it back off the session's budget.
|
|
96771
|
+
onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
|
|
96585
96772
|
});
|
|
96586
96773
|
let balanceMonitor;
|
|
96587
96774
|
if (options._balanceMonitorOverride) {
|
|
@@ -97865,6 +98052,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
97865
98052
|
return { success: true, response };
|
|
97866
98053
|
} catch (err) {
|
|
97867
98054
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
98055
|
+
if (err instanceof CostCapError) {
|
|
98056
|
+
return {
|
|
98057
|
+
success: false,
|
|
98058
|
+
errorBody: JSON.stringify({
|
|
98059
|
+
error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
|
|
98060
|
+
}),
|
|
98061
|
+
errorStatus: 429,
|
|
98062
|
+
isProviderError: false,
|
|
98063
|
+
errorCategory: "payment_error"
|
|
98064
|
+
};
|
|
98065
|
+
}
|
|
97868
98066
|
if (err instanceof SpendPolicyError) {
|
|
97869
98067
|
return {
|
|
97870
98068
|
success: false,
|
|
@@ -98944,14 +99142,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98944
99142
|
}
|
|
98945
99143
|
}
|
|
98946
99144
|
}
|
|
99145
|
+
let sessionSpend;
|
|
98947
99146
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
|
|
98948
|
-
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98949
99147
|
const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
|
|
98950
|
-
const
|
|
98951
|
-
|
|
98952
|
-
|
|
99148
|
+
const admission = SessionSpend.admit(
|
|
99149
|
+
sessionStore,
|
|
99150
|
+
effectiveSessionId,
|
|
99151
|
+
usdToMicros(options.maxCostPerRunUsd),
|
|
99152
|
+
parseMicros(thisReqEstStr) ?? 0n
|
|
99153
|
+
);
|
|
99154
|
+
if (!admission.admitted) {
|
|
99155
|
+
const spentUsd = microsToUsd(admission.spentMicros);
|
|
99156
|
+
const heldUsd = microsToUsd(admission.heldMicros);
|
|
99157
|
+
const estUsd = microsToUsd(admission.estimateMicros);
|
|
99158
|
+
const projectedCostUsd = spentUsd + heldUsd + estUsd;
|
|
99159
|
+
const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
|
|
98953
99160
|
console.log(
|
|
98954
|
-
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (
|
|
99161
|
+
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
|
|
98955
99162
|
);
|
|
98956
99163
|
res.writeHead(429, {
|
|
98957
99164
|
"Content-Type": "application/json",
|
|
@@ -98960,7 +99167,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98960
99167
|
res.end(
|
|
98961
99168
|
JSON.stringify({
|
|
98962
99169
|
error: {
|
|
98963
|
-
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (
|
|
99170
|
+
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
|
|
98964
99171
|
type: "cost_cap_exceeded",
|
|
98965
99172
|
code: "cost_cap_exceeded"
|
|
98966
99173
|
}
|
|
@@ -98969,7 +99176,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98969
99176
|
deduplicator.removeInflight(dedupKey);
|
|
98970
99177
|
return;
|
|
98971
99178
|
}
|
|
99179
|
+
sessionSpend = admission.spend;
|
|
99180
|
+
} else if (options.maxCostPerRunUsd && effectiveSessionId) {
|
|
99181
|
+
sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
|
|
98972
99182
|
}
|
|
99183
|
+
const requestStore = paymentStore.getStore();
|
|
99184
|
+
if (requestStore) requestStore.sessionSpend = sessionSpend;
|
|
98973
99185
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
|
|
98974
99186
|
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98975
99187
|
const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
|
|
@@ -99365,11 +99577,9 @@ data: [DONE]
|
|
|
99365
99577
|
actualModelUsed = tryModel;
|
|
99366
99578
|
recordOutcome(tryModel, requestKind, true);
|
|
99367
99579
|
console.log(`[ClawRouter] Success with model: ${tryModel}`);
|
|
99368
|
-
if (
|
|
99369
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99370
|
-
if (costEst)
|
|
99371
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99372
|
-
}
|
|
99580
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99581
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99582
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99373
99583
|
}
|
|
99374
99584
|
break;
|
|
99375
99585
|
}
|
|
@@ -99443,11 +99653,9 @@ data: [DONE]
|
|
|
99443
99653
|
upstream = retryResult.response;
|
|
99444
99654
|
actualModelUsed = tryModel;
|
|
99445
99655
|
console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
|
|
99446
|
-
if (
|
|
99447
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99448
|
-
if (costEst)
|
|
99449
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99450
|
-
}
|
|
99656
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99657
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99658
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99451
99659
|
}
|
|
99452
99660
|
break;
|
|
99453
99661
|
}
|
|
@@ -99514,11 +99722,9 @@ data: [DONE]
|
|
|
99514
99722
|
upstream = retryResult.response;
|
|
99515
99723
|
actualModelUsed = tryModel;
|
|
99516
99724
|
console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
|
|
99517
|
-
if (
|
|
99518
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99519
|
-
if (costEst)
|
|
99520
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99521
|
-
}
|
|
99725
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99726
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99727
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99522
99728
|
}
|
|
99523
99729
|
break;
|
|
99524
99730
|
}
|
|
@@ -99612,6 +99818,7 @@ data: [DONE]
|
|
|
99612
99818
|
);
|
|
99613
99819
|
const rawErrBody = lastError?.body || structuredMessage;
|
|
99614
99820
|
const errStatus = lastError?.status || 502;
|
|
99821
|
+
const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
|
|
99615
99822
|
const transformedErr = transformPaymentError(rawErrBody);
|
|
99616
99823
|
requestHadError = true;
|
|
99617
99824
|
if (headersSentEarly) {
|
|
@@ -99648,7 +99855,8 @@ data: [DONE]
|
|
|
99648
99855
|
"Content-Type": "application/json",
|
|
99649
99856
|
"x-context-used-kb": String(originalContextSizeKB),
|
|
99650
99857
|
"x-context-limit-kb": String(CONTEXT_LIMIT_KB),
|
|
99651
|
-
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
|
|
99858
|
+
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
|
|
99859
|
+
...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
|
|
99652
99860
|
});
|
|
99653
99861
|
res.end(transformedErr);
|
|
99654
99862
|
deduplicator.complete(dedupKey, {
|
|
@@ -100082,6 +100290,8 @@ data: [DONE]
|
|
|
100082
100290
|
throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
|
|
100083
100291
|
}
|
|
100084
100292
|
throw err;
|
|
100293
|
+
} finally {
|
|
100294
|
+
sessionSpend?.release();
|
|
100085
100295
|
}
|
|
100086
100296
|
const logModel = routingDecision?.model ?? modelId;
|
|
100087
100297
|
if (logModel) {
|
|
@@ -100168,6 +100378,7 @@ var init_proxy = __esm({
|
|
|
100168
100378
|
init_auth();
|
|
100169
100379
|
init_api_key();
|
|
100170
100380
|
init_spend_control();
|
|
100381
|
+
init_session_spend();
|
|
100171
100382
|
init_compression();
|
|
100172
100383
|
init_errors5();
|
|
100173
100384
|
init_version4();
|
|
@@ -101641,7 +101852,8 @@ __export(reconcile_exports, {
|
|
|
101641
101852
|
joinRows: () => joinRows,
|
|
101642
101853
|
loadGatewayRows: () => loadGatewayRows,
|
|
101643
101854
|
loadLocalRows: () => loadLocalRows,
|
|
101644
|
-
reconcile: () => reconcile
|
|
101855
|
+
reconcile: () => reconcile,
|
|
101856
|
+
reconcileExitCode: () => reconcileExitCode
|
|
101645
101857
|
});
|
|
101646
101858
|
import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
101647
101859
|
import { join as join13 } from "path";
|
|
@@ -101688,16 +101900,36 @@ async function loadLocalRows(since) {
|
|
|
101688
101900
|
async function loadGatewayRows(apiKey, from15, maxPages = 40) {
|
|
101689
101901
|
const rows = [];
|
|
101690
101902
|
const unavailableDays = /* @__PURE__ */ new Set();
|
|
101903
|
+
const requested = /* @__PURE__ */ new Set();
|
|
101691
101904
|
let cursor;
|
|
101692
101905
|
for (let page = 0; page < maxPages; page++) {
|
|
101693
101906
|
const result = await fetchUsagePage(apiKey, { from: from15, limit: 500, cursor });
|
|
101694
|
-
if (!result)
|
|
101907
|
+
if (!result) {
|
|
101908
|
+
if (page === 0) return void 0;
|
|
101909
|
+
return {
|
|
101910
|
+
rows,
|
|
101911
|
+
unavailableDays: [...unavailableDays],
|
|
101912
|
+
incomplete: `page ${page + 1} of the ledger could not be read`
|
|
101913
|
+
};
|
|
101914
|
+
}
|
|
101695
101915
|
rows.push(...result.rows);
|
|
101696
101916
|
result.unavailableDays.forEach((d) => unavailableDays.add(d));
|
|
101697
|
-
if (!result.nextCursor)
|
|
101917
|
+
if (!result.nextCursor) return { rows, unavailableDays: [...unavailableDays] };
|
|
101918
|
+
if (requested.has(result.nextCursor)) {
|
|
101919
|
+
return {
|
|
101920
|
+
rows,
|
|
101921
|
+
unavailableDays: [...unavailableDays],
|
|
101922
|
+
incomplete: `the ledger's page cursor stopped advancing after page ${page + 1}`
|
|
101923
|
+
};
|
|
101924
|
+
}
|
|
101698
101925
|
cursor = result.nextCursor;
|
|
101926
|
+
requested.add(cursor);
|
|
101699
101927
|
}
|
|
101700
|
-
return {
|
|
101928
|
+
return {
|
|
101929
|
+
rows,
|
|
101930
|
+
unavailableDays: [...unavailableDays],
|
|
101931
|
+
incomplete: `the ledger has more than ${maxPages} pages for this window; try fewer --days`
|
|
101932
|
+
};
|
|
101701
101933
|
}
|
|
101702
101934
|
function joinRows(localRows, gatewayRows, unkeyed = 0, unavailableDays = []) {
|
|
101703
101935
|
const localById = new Map(localRows.map((r2) => [r2.requestId, r2]));
|
|
@@ -101740,7 +101972,14 @@ async function reconcile(apiKey, days) {
|
|
|
101740
101972
|
loadGatewayRows(apiKey, since.toISOString())
|
|
101741
101973
|
]);
|
|
101742
101974
|
if (!gateway) throw new Error("Could not read the BlockRun usage ledger (GET /v1/usage).");
|
|
101743
|
-
|
|
101975
|
+
const result = joinRows(localRows, gateway.rows, unkeyed, gateway.unavailableDays);
|
|
101976
|
+
if (gateway.incomplete) result.ledgerIncomplete = gateway.incomplete;
|
|
101977
|
+
return result;
|
|
101978
|
+
}
|
|
101979
|
+
function reconcileExitCode(r2) {
|
|
101980
|
+
if (r2.chargedNotRecorded.length > 0) return 2;
|
|
101981
|
+
if (r2.ledgerIncomplete) return 1;
|
|
101982
|
+
return 0;
|
|
101744
101983
|
}
|
|
101745
101984
|
function formatReconcile(r2, days) {
|
|
101746
101985
|
const out = [];
|
|
@@ -101748,6 +101987,13 @@ function formatReconcile(r2, days) {
|
|
|
101748
101987
|
out.push(`
|
|
101749
101988
|
BlockRun reconciliation \u2014 last ${days} day${days === 1 ? "" : "s"}
|
|
101750
101989
|
`);
|
|
101990
|
+
if (r2.ledgerIncomplete) {
|
|
101991
|
+
out.push(` \u26A0 Partial ledger: ${r2.ledgerIncomplete}.`);
|
|
101992
|
+
out.push(` Only the ledger pages that were read are compared: the gateway total is`);
|
|
101993
|
+
out.push(` short, charges on the unread pages are not checked, and calls billed on`);
|
|
101994
|
+
out.push(` those pages show up as recorded locally with no settled ledger row.
|
|
101995
|
+
`);
|
|
101996
|
+
}
|
|
101751
101997
|
out.push(
|
|
101752
101998
|
` Gateway charged: ${usd(r2.gatewayTotalUsd)} (${r2.matched.length + r2.chargedNotRecorded.length} settled calls)`
|
|
101753
101999
|
);
|
|
@@ -123261,7 +123507,7 @@ var init_client5 = __esm({
|
|
|
123261
123507
|
function resolveSpendControl(deps) {
|
|
123262
123508
|
return deps?.spendControl ?? getSharedSpendControl();
|
|
123263
123509
|
}
|
|
123264
|
-
function
|
|
123510
|
+
function usdToMicros2(usd2) {
|
|
123265
123511
|
return String(Math.ceil(usd2 * 1e6));
|
|
123266
123512
|
}
|
|
123267
123513
|
async function signUnderSpendPolicy(deps, quote, action, sign4) {
|
|
@@ -123527,7 +123773,7 @@ Re-call with confirm:true to sign and submit.`,
|
|
|
123527
123773
|
payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
|
|
123528
123774
|
network: `eip155:${POLYGON_CHAIN_ID}`,
|
|
123529
123775
|
asset: PUSD_COLLATERAL,
|
|
123530
|
-
amount:
|
|
123776
|
+
amount: usdToMicros2(notional)
|
|
123531
123777
|
};
|
|
123532
123778
|
let r2;
|
|
123533
123779
|
try {
|
|
@@ -235914,10 +236160,10 @@ async function cmdReconcile(days) {
|
|
|
235914
236160
|
process.exit(1);
|
|
235915
236161
|
}
|
|
235916
236162
|
try {
|
|
235917
|
-
const { reconcile: reconcile2, formatReconcile: formatReconcile2 } = await Promise.resolve().then(() => (init_reconcile(), reconcile_exports));
|
|
236163
|
+
const { reconcile: reconcile2, formatReconcile: formatReconcile2, reconcileExitCode: reconcileExitCode2 } = await Promise.resolve().then(() => (init_reconcile(), reconcile_exports));
|
|
235918
236164
|
const result = await reconcile2(resolved.key, days);
|
|
235919
236165
|
console.log(formatReconcile2(result, days));
|
|
235920
|
-
process.exitCode = result
|
|
236166
|
+
process.exitCode = reconcileExitCode2(result);
|
|
235921
236167
|
} catch (error) {
|
|
235922
236168
|
console.error(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
|
|
235923
236169
|
process.exit(1);
|
package/dist/index.d.ts
CHANGED
|
@@ -966,6 +966,7 @@ declare const DEFAULT_SESSION_CONFIG: SessionConfig;
|
|
|
966
966
|
*/
|
|
967
967
|
declare class SessionStore {
|
|
968
968
|
private sessions;
|
|
969
|
+
private sessionHolds;
|
|
969
970
|
private config;
|
|
970
971
|
private cleanupInterval;
|
|
971
972
|
constructor(config?: Partial<SessionConfig>);
|
|
@@ -1036,6 +1037,19 @@ declare class SessionStore {
|
|
|
1036
1037
|
* Get the total accumulated cost for a session in USD.
|
|
1037
1038
|
*/
|
|
1038
1039
|
getSessionCostUsd(sessionId: string): number;
|
|
1040
|
+
/**
|
|
1041
|
+
* Get the total accumulated cost for a session in USDC micros.
|
|
1042
|
+
*/
|
|
1043
|
+
getSessionCostMicros(sessionId: string): bigint;
|
|
1044
|
+
/**
|
|
1045
|
+
* Budget held by a session's in-flight requests (USDC micros) — admitted
|
|
1046
|
+
* against the strict cap but not yet spent. See session-spend.ts.
|
|
1047
|
+
*/
|
|
1048
|
+
getSessionHoldMicros(sessionId: string): bigint;
|
|
1049
|
+
/**
|
|
1050
|
+
* Grow (positive) or release (negative) a session's in-flight hold.
|
|
1051
|
+
*/
|
|
1052
|
+
adjustSessionHold(sessionId: string, deltaMicros: bigint): void;
|
|
1039
1053
|
/**
|
|
1040
1054
|
* Stop the cleanup interval.
|
|
1041
1055
|
*/
|