@routstr/sdk 0.2.3 → 0.2.5
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 +9 -0
- package/dist/client/index.d.mts +21 -8
- package/dist/client/index.d.ts +21 -8
- package/dist/client/index.js +1406 -69
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1406 -70
- package/dist/client/index.mjs.map +1 -1
- package/dist/discovery/index.d.mts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +1 -4
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/index.mjs +1 -4
- package/dist/discovery/index.mjs.map +1 -1
- package/dist/index.d.mts +15 -19
- package/dist/index.d.ts +15 -19
- package/dist/index.js +2385 -1574
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2380 -1575
- package/dist/index.mjs.map +1 -1
- package/dist/{interfaces-DGdP8fQp.d.mts → interfaces-BWJJTCXO.d.mts} +1 -1
- package/dist/{interfaces-CC0LT9p9.d.ts → interfaces-BxDEka72.d.ts} +1 -1
- package/dist/{interfaces-B85Wx7ni.d.mts → interfaces-C6Dr6hKy.d.mts} +1 -1
- package/dist/{interfaces-BVNyAmKu.d.ts → interfaces-CluftN4z.d.ts} +1 -1
- package/dist/storage/index.d.mts +56 -34
- package/dist/storage/index.d.ts +56 -34
- package/dist/storage/index.js +500 -51
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/index.mjs +497 -52
- package/dist/storage/index.mjs.map +1 -1
- package/dist/{types-BlHjmWRK.d.mts → types-BYj_8c5c.d.mts} +3 -0
- package/dist/{types-BlHjmWRK.d.ts → types-BYj_8c5c.d.ts} +3 -0
- package/dist/wallet/index.d.mts +9 -5
- package/dist/wallet/index.d.ts +9 -5
- package/dist/wallet/index.js +54 -22
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +54 -22
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var applesauceCore = require('applesauce-core');
|
|
|
5
5
|
var rxjs = require('rxjs');
|
|
6
6
|
var vanilla = require('zustand/vanilla');
|
|
7
7
|
var cashuTs = require('@cashu/cashu-ts');
|
|
8
|
+
var stream = require('stream');
|
|
8
9
|
|
|
9
10
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
11
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -385,10 +386,7 @@ var ModelManager = class _ModelManager {
|
|
|
385
386
|
throw new Error(`Failed to fetch models: ${res.status}`);
|
|
386
387
|
}
|
|
387
388
|
const json = await res.json();
|
|
388
|
-
const list = Array.isArray(json?.data) ? json.data
|
|
389
|
-
...m,
|
|
390
|
-
id: m.id.split("/").pop() || m.id
|
|
391
|
-
})) : [];
|
|
389
|
+
const list = Array.isArray(json?.data) ? json.data : [];
|
|
392
390
|
return list;
|
|
393
391
|
}
|
|
394
392
|
isProviderDownError(error) {
|
|
@@ -1115,7 +1113,7 @@ var CashuSpender = class {
|
|
|
1115
1113
|
/**
|
|
1116
1114
|
* Refund specific providers without retrying spend
|
|
1117
1115
|
*/
|
|
1118
|
-
async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
|
|
1116
|
+
async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
|
|
1119
1117
|
const results = [];
|
|
1120
1118
|
const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
|
|
1121
1119
|
const toRefund = pendingDistribution.filter(
|
|
@@ -1165,7 +1163,8 @@ var CashuSpender = class {
|
|
|
1165
1163
|
const refundResult = await this.balanceManager.refundApiKey({
|
|
1166
1164
|
mintUrl,
|
|
1167
1165
|
baseUrl: apiKeyEntry.baseUrl,
|
|
1168
|
-
apiKey: apiKeyEntryFull.key
|
|
1166
|
+
apiKey: apiKeyEntryFull.key,
|
|
1167
|
+
forceRefund
|
|
1169
1168
|
});
|
|
1170
1169
|
if (refundResult.success) {
|
|
1171
1170
|
this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
|
|
@@ -1338,12 +1337,29 @@ var BalanceManager = class {
|
|
|
1338
1337
|
}
|
|
1339
1338
|
/**
|
|
1340
1339
|
* Refund API key balance - convert remaining API key balance to cashu token
|
|
1340
|
+
* @param options - Refund options including forceRefund flag
|
|
1341
|
+
* @returns Refund result
|
|
1341
1342
|
*/
|
|
1342
1343
|
async refundApiKey(options) {
|
|
1343
|
-
const { mintUrl, baseUrl, apiKey } = options;
|
|
1344
|
+
const { mintUrl, baseUrl, apiKey, forceRefund } = options;
|
|
1344
1345
|
if (!apiKey) {
|
|
1345
1346
|
return { success: false, message: "No API key to refund" };
|
|
1346
1347
|
}
|
|
1348
|
+
if (!forceRefund) {
|
|
1349
|
+
const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
1350
|
+
if (apiKeyEntry?.lastUsed) {
|
|
1351
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
|
|
1352
|
+
if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
|
|
1353
|
+
console.log(
|
|
1354
|
+
`[BalanceManager] Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`
|
|
1355
|
+
);
|
|
1356
|
+
return {
|
|
1357
|
+
success: false,
|
|
1358
|
+
message: "API key was used recently, skipping refund"
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1347
1363
|
let fetchResult;
|
|
1348
1364
|
try {
|
|
1349
1365
|
fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
|
|
@@ -1515,9 +1531,13 @@ var BalanceManager = class {
|
|
|
1515
1531
|
p2pkPubkey
|
|
1516
1532
|
} = options;
|
|
1517
1533
|
const adjustedAmount = Math.ceil(amount);
|
|
1518
|
-
console.log(
|
|
1534
|
+
console.log(
|
|
1535
|
+
`[BalanceManager.createProviderToken] Starting: baseUrl=${baseUrl}, mintUrl=${mintUrl}, amount=${amount}, adjustedAmount=${adjustedAmount}, retryCount=${retryCount}`
|
|
1536
|
+
);
|
|
1519
1537
|
if (!adjustedAmount || isNaN(adjustedAmount)) {
|
|
1520
|
-
console.error(
|
|
1538
|
+
console.error(
|
|
1539
|
+
`[BalanceManager.createProviderToken] FAILURE: Invalid amount - amount=${amount}, adjustedAmount=${adjustedAmount}`
|
|
1540
|
+
);
|
|
1521
1541
|
return { success: false, error: "Invalid top up amount" };
|
|
1522
1542
|
}
|
|
1523
1543
|
const balanceState = await this.getBalanceState();
|
|
@@ -1531,8 +1551,8 @@ var BalanceManager = class {
|
|
|
1531
1551
|
const refundableProviderBalance = Object.entries(
|
|
1532
1552
|
balanceState.providerBalances
|
|
1533
1553
|
).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
|
|
1534
|
-
if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount <
|
|
1535
|
-
await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
|
|
1554
|
+
if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
|
|
1555
|
+
await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
|
|
1536
1556
|
return this.createProviderToken({
|
|
1537
1557
|
...options,
|
|
1538
1558
|
retryCount: retryCount + 1
|
|
@@ -1548,15 +1568,11 @@ var BalanceManager = class {
|
|
|
1548
1568
|
{ url: "", balance: 0 }
|
|
1549
1569
|
).url
|
|
1550
1570
|
);
|
|
1551
|
-
console.error(
|
|
1571
|
+
console.error(
|
|
1572
|
+
`[BalanceManager.createProviderToken] FAILURE: Insufficient balance - required=${adjustedAmount}, available=${totalMintBalance + targetProviderBalance}, totalMintBalance=${totalMintBalance}, targetProviderBalance=${targetProviderBalance}, refundableProviderBalance=${refundableProviderBalance}`
|
|
1573
|
+
);
|
|
1552
1574
|
return { success: false, error: error.message };
|
|
1553
1575
|
}
|
|
1554
|
-
if (targetProviderBalance >= adjustedAmount) {
|
|
1555
|
-
return {
|
|
1556
|
-
success: true,
|
|
1557
|
-
amountSpent: 0
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
1576
|
const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
|
|
1561
1577
|
let requiredAmount = adjustedAmount;
|
|
1562
1578
|
const supportedMintsOnly = providerMints.length > 0;
|
|
@@ -1590,7 +1606,9 @@ var BalanceManager = class {
|
|
|
1590
1606
|
maxMintUrl = mintUrl2;
|
|
1591
1607
|
}
|
|
1592
1608
|
}
|
|
1593
|
-
console.error(
|
|
1609
|
+
console.error(
|
|
1610
|
+
`[BalanceManager.createProviderToken] FAILURE: No candidate mints found - requiredAmount=${requiredAmount}, totalMintBalance=${totalMintBalance}, maxBalance=${maxBalance}, maxMintUrl=${maxMintUrl}, providerMints=${JSON.stringify(providerMints)}`
|
|
1611
|
+
);
|
|
1594
1612
|
const error = new InsufficientBalanceError(
|
|
1595
1613
|
adjustedAmount,
|
|
1596
1614
|
totalMintBalance,
|
|
@@ -1602,13 +1620,17 @@ var BalanceManager = class {
|
|
|
1602
1620
|
let lastError;
|
|
1603
1621
|
for (const candidateMint of candidates) {
|
|
1604
1622
|
try {
|
|
1605
|
-
console.log(
|
|
1623
|
+
console.log(
|
|
1624
|
+
`[BalanceManager.createProviderToken] Attempting mint: ${candidateMint}, amount: ${requiredAmount}`
|
|
1625
|
+
);
|
|
1606
1626
|
const token = await this.walletAdapter.sendToken(
|
|
1607
1627
|
candidateMint,
|
|
1608
1628
|
requiredAmount,
|
|
1609
1629
|
p2pkPubkey
|
|
1610
1630
|
);
|
|
1611
|
-
console.log(
|
|
1631
|
+
console.log(
|
|
1632
|
+
`[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`
|
|
1633
|
+
);
|
|
1612
1634
|
return {
|
|
1613
1635
|
success: true,
|
|
1614
1636
|
token,
|
|
@@ -1617,11 +1639,15 @@ var BalanceManager = class {
|
|
|
1617
1639
|
};
|
|
1618
1640
|
} catch (error) {
|
|
1619
1641
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1620
|
-
console.error(
|
|
1642
|
+
console.error(
|
|
1643
|
+
`[BalanceManager.createProviderToken] FAILURE: Mint ${candidateMint} failed with error: ${errorMsg}`
|
|
1644
|
+
);
|
|
1621
1645
|
if (error instanceof Error) {
|
|
1622
1646
|
lastError = errorMsg;
|
|
1623
1647
|
if (isNetworkErrorMessage(error.message)) {
|
|
1624
|
-
console.warn(
|
|
1648
|
+
console.warn(
|
|
1649
|
+
`[BalanceManager.createProviderToken] Network error from ${candidateMint}, trying next mint...`
|
|
1650
|
+
);
|
|
1625
1651
|
continue;
|
|
1626
1652
|
}
|
|
1627
1653
|
}
|
|
@@ -1631,7 +1657,9 @@ var BalanceManager = class {
|
|
|
1631
1657
|
};
|
|
1632
1658
|
}
|
|
1633
1659
|
}
|
|
1634
|
-
console.error(
|
|
1660
|
+
console.error(
|
|
1661
|
+
`[BalanceManager.createProviderToken] FAILURE: All candidate mints exhausted - lastError=${lastError}, candidates=${JSON.stringify(candidates)}`
|
|
1662
|
+
);
|
|
1635
1663
|
return {
|
|
1636
1664
|
success: false,
|
|
1637
1665
|
error: lastError || "All candidate mints failed while creating top up token"
|
|
@@ -1677,9 +1705,10 @@ var BalanceManager = class {
|
|
|
1677
1705
|
}
|
|
1678
1706
|
return candidates;
|
|
1679
1707
|
}
|
|
1680
|
-
async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
|
|
1708
|
+
async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
|
|
1681
1709
|
const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
|
|
1682
1710
|
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
1711
|
+
const forceRefund = retryCount >= 2;
|
|
1683
1712
|
const toRefund = pendingDistribution.filter(
|
|
1684
1713
|
(pending) => pending.baseUrl !== baseUrl
|
|
1685
1714
|
);
|
|
@@ -1720,7 +1749,8 @@ var BalanceManager = class {
|
|
|
1720
1749
|
const result = await this.refundApiKey({
|
|
1721
1750
|
mintUrl,
|
|
1722
1751
|
baseUrl: apiKeyEntry.baseUrl,
|
|
1723
|
-
apiKey: fullApiKeyEntry.key
|
|
1752
|
+
apiKey: fullApiKeyEntry.key,
|
|
1753
|
+
forceRefund
|
|
1724
1754
|
});
|
|
1725
1755
|
return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
|
|
1726
1756
|
})
|
|
@@ -1974,6 +2004,77 @@ var BalanceManager = class {
|
|
|
1974
2004
|
}
|
|
1975
2005
|
};
|
|
1976
2006
|
|
|
2007
|
+
// client/usage.ts
|
|
2008
|
+
function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
|
|
2009
|
+
if (!body || typeof body !== "object") return null;
|
|
2010
|
+
const usage = body.usage;
|
|
2011
|
+
if (!usage || typeof usage !== "object") return null;
|
|
2012
|
+
const promptTokens = Number(usage.prompt_tokens ?? 0);
|
|
2013
|
+
const completionTokens = Number(usage.completion_tokens ?? 0);
|
|
2014
|
+
const totalTokens = Number(usage.total_tokens ?? 0);
|
|
2015
|
+
const costValue = usage.cost;
|
|
2016
|
+
let cost = 0;
|
|
2017
|
+
let satsCost = fallbackSatsCost;
|
|
2018
|
+
if (typeof costValue === "number") {
|
|
2019
|
+
cost = costValue;
|
|
2020
|
+
} else if (costValue && typeof costValue === "object") {
|
|
2021
|
+
const costObj = costValue;
|
|
2022
|
+
const totalUsd = costObj.total_usd;
|
|
2023
|
+
const totalMsats = costObj.total_msats;
|
|
2024
|
+
cost = typeof totalUsd === "number" ? totalUsd : 0;
|
|
2025
|
+
if (typeof totalMsats === "number") {
|
|
2026
|
+
satsCost = totalMsats / 1e3;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
promptTokens,
|
|
2034
|
+
completionTokens,
|
|
2035
|
+
totalTokens,
|
|
2036
|
+
cost,
|
|
2037
|
+
satsCost
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function extractResponseId(body) {
|
|
2041
|
+
if (!body || typeof body !== "object") return void 0;
|
|
2042
|
+
const id = body.id;
|
|
2043
|
+
if (typeof id !== "string") return void 0;
|
|
2044
|
+
const trimmed = id.trim();
|
|
2045
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2046
|
+
}
|
|
2047
|
+
function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
|
|
2048
|
+
if (!parsed || typeof parsed !== "object" || !parsed.usage) {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
const usage = parsed.usage;
|
|
2052
|
+
const usageCost = usage.cost;
|
|
2053
|
+
const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
|
|
2054
|
+
const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
|
|
2055
|
+
const result = {
|
|
2056
|
+
promptTokens: Number(usage.prompt_tokens ?? 0),
|
|
2057
|
+
completionTokens: Number(usage.completion_tokens ?? 0),
|
|
2058
|
+
totalTokens: Number(usage.total_tokens ?? 0),
|
|
2059
|
+
cost: Number(cost ?? 0),
|
|
2060
|
+
satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
|
|
2061
|
+
};
|
|
2062
|
+
if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
return result;
|
|
2066
|
+
}
|
|
2067
|
+
function toUsageStats(usage) {
|
|
2068
|
+
if (!usage) return void 0;
|
|
2069
|
+
return {
|
|
2070
|
+
total_tokens: usage.totalTokens,
|
|
2071
|
+
prompt_tokens: usage.promptTokens,
|
|
2072
|
+
completion_tokens: usage.completionTokens,
|
|
2073
|
+
cost: usage.cost,
|
|
2074
|
+
sats_cost: usage.satsCost
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1977
2078
|
// client/StreamProcessor.ts
|
|
1978
2079
|
var StreamProcessor = class {
|
|
1979
2080
|
accumulatedContent = "";
|
|
@@ -2001,6 +2102,7 @@ var StreamProcessor = class {
|
|
|
2001
2102
|
let finish_reason;
|
|
2002
2103
|
let citations;
|
|
2003
2104
|
let annotations;
|
|
2105
|
+
let responseId;
|
|
2004
2106
|
try {
|
|
2005
2107
|
while (true) {
|
|
2006
2108
|
const { done, value } = await reader.read();
|
|
@@ -2029,6 +2131,9 @@ var StreamProcessor = class {
|
|
|
2029
2131
|
if (parsed.finish_reason) {
|
|
2030
2132
|
finish_reason = parsed.finish_reason;
|
|
2031
2133
|
}
|
|
2134
|
+
if (parsed.responseId) {
|
|
2135
|
+
responseId = parsed.responseId;
|
|
2136
|
+
}
|
|
2032
2137
|
if (parsed.citations) {
|
|
2033
2138
|
citations = parsed.citations;
|
|
2034
2139
|
}
|
|
@@ -2049,6 +2154,7 @@ var StreamProcessor = class {
|
|
|
2049
2154
|
images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
|
|
2050
2155
|
usage,
|
|
2051
2156
|
model,
|
|
2157
|
+
responseId,
|
|
2052
2158
|
finish_reason,
|
|
2053
2159
|
citations,
|
|
2054
2160
|
annotations
|
|
@@ -2076,12 +2182,15 @@ var StreamProcessor = class {
|
|
|
2076
2182
|
result.reasoning = parsed.choices[0].delta.reasoning;
|
|
2077
2183
|
}
|
|
2078
2184
|
if (parsed.usage) {
|
|
2079
|
-
result.usage = {
|
|
2185
|
+
result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
|
|
2080
2186
|
total_tokens: parsed.usage.total_tokens,
|
|
2081
2187
|
prompt_tokens: parsed.usage.prompt_tokens,
|
|
2082
2188
|
completion_tokens: parsed.usage.completion_tokens
|
|
2083
2189
|
};
|
|
2084
2190
|
}
|
|
2191
|
+
if (parsed.id) {
|
|
2192
|
+
result.responseId = parsed.id;
|
|
2193
|
+
}
|
|
2085
2194
|
if (parsed.model) {
|
|
2086
2195
|
result.model = parsed.model;
|
|
2087
2196
|
}
|
|
@@ -2529,7 +2638,6 @@ var ProviderManager = class _ProviderManager {
|
|
|
2529
2638
|
* Get providers for a model sorted by prompt+completion pricing
|
|
2530
2639
|
*/
|
|
2531
2640
|
getProviderPriceRankingForModel(modelId, options = {}) {
|
|
2532
|
-
const normalizedId = this.normalizeModelId(modelId);
|
|
2533
2641
|
const includeDisabled = options.includeDisabled ?? false;
|
|
2534
2642
|
const torMode = options.torMode ?? false;
|
|
2535
2643
|
const disabledProviders = new Set(
|
|
@@ -2543,9 +2651,7 @@ var ProviderManager = class _ProviderManager {
|
|
|
2543
2651
|
if (torMode && !baseUrl.includes(".onion")) continue;
|
|
2544
2652
|
if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
|
|
2545
2653
|
continue;
|
|
2546
|
-
const match = models.find(
|
|
2547
|
-
(model) => this.normalizeModelId(model.id) === normalizedId
|
|
2548
|
-
);
|
|
2654
|
+
const match = models.find((model) => model.id === modelId);
|
|
2549
2655
|
if (!match?.sats_pricing) continue;
|
|
2550
2656
|
const prompt = match.sats_pricing.prompt;
|
|
2551
2657
|
const completion = match.sats_pricing.completion;
|
|
@@ -2658,1323 +2764,740 @@ var ProviderManager = class _ProviderManager {
|
|
|
2658
2764
|
}
|
|
2659
2765
|
};
|
|
2660
2766
|
|
|
2661
|
-
//
|
|
2662
|
-
var
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
);
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
return this.mode;
|
|
2696
|
-
}
|
|
2697
|
-
getDebugLevel() {
|
|
2698
|
-
return this.debugLevel;
|
|
2699
|
-
}
|
|
2700
|
-
setDebugLevel(level) {
|
|
2701
|
-
this.debugLevel = level;
|
|
2702
|
-
}
|
|
2703
|
-
_log(level, ...args) {
|
|
2704
|
-
const levelPriority = {
|
|
2705
|
-
DEBUG: 0,
|
|
2706
|
-
WARN: 1,
|
|
2707
|
-
ERROR: 2
|
|
2708
|
-
};
|
|
2709
|
-
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
2710
|
-
switch (level) {
|
|
2711
|
-
case "DEBUG":
|
|
2712
|
-
console.log(...args);
|
|
2713
|
-
break;
|
|
2714
|
-
case "WARN":
|
|
2715
|
-
console.warn(...args);
|
|
2716
|
-
break;
|
|
2717
|
-
case "ERROR":
|
|
2718
|
-
console.error(...args);
|
|
2719
|
-
break;
|
|
2767
|
+
// storage/drivers/localStorage.ts
|
|
2768
|
+
var canUseLocalStorage = () => {
|
|
2769
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
2770
|
+
};
|
|
2771
|
+
var isQuotaExceeded = (error) => {
|
|
2772
|
+
const e = error;
|
|
2773
|
+
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
2774
|
+
};
|
|
2775
|
+
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
2776
|
+
var localStorageDriver = {
|
|
2777
|
+
async getItem(key, defaultValue) {
|
|
2778
|
+
if (!canUseLocalStorage()) return defaultValue;
|
|
2779
|
+
try {
|
|
2780
|
+
const item = window.localStorage.getItem(key);
|
|
2781
|
+
if (item === null) return defaultValue;
|
|
2782
|
+
try {
|
|
2783
|
+
return JSON.parse(item);
|
|
2784
|
+
} catch (parseError) {
|
|
2785
|
+
if (typeof defaultValue === "string") {
|
|
2786
|
+
return item;
|
|
2787
|
+
}
|
|
2788
|
+
throw parseError;
|
|
2789
|
+
}
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
console.error(`Error retrieving item with key "${key}":`, error);
|
|
2792
|
+
if (canUseLocalStorage()) {
|
|
2793
|
+
try {
|
|
2794
|
+
window.localStorage.removeItem(key);
|
|
2795
|
+
} catch (removeError) {
|
|
2796
|
+
console.error(
|
|
2797
|
+
`Error removing corrupted item with key "${key}":`,
|
|
2798
|
+
removeError
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2720
2801
|
}
|
|
2802
|
+
return defaultValue;
|
|
2803
|
+
}
|
|
2804
|
+
},
|
|
2805
|
+
async setItem(key, value) {
|
|
2806
|
+
if (!canUseLocalStorage()) return;
|
|
2807
|
+
try {
|
|
2808
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
if (isQuotaExceeded(error)) {
|
|
2811
|
+
if (NON_CRITICAL_KEYS.has(key)) {
|
|
2812
|
+
console.warn(
|
|
2813
|
+
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
2814
|
+
);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
try {
|
|
2818
|
+
window.localStorage.removeItem("modelsFromAllProviders");
|
|
2819
|
+
} catch {
|
|
2820
|
+
}
|
|
2821
|
+
try {
|
|
2822
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2823
|
+
return;
|
|
2824
|
+
} catch (retryError) {
|
|
2825
|
+
console.warn(
|
|
2826
|
+
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
2827
|
+
retryError
|
|
2828
|
+
);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
console.error(`Error storing item with key "${key}":`, error);
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
async removeItem(key) {
|
|
2836
|
+
if (!canUseLocalStorage()) return;
|
|
2837
|
+
try {
|
|
2838
|
+
window.localStorage.removeItem(key);
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
console.error(`Error removing item with key "${key}":`, error);
|
|
2721
2841
|
}
|
|
2722
2842
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
getBalanceManager() {
|
|
2733
|
-
return this.balanceManager;
|
|
2734
|
-
}
|
|
2735
|
-
/**
|
|
2736
|
-
* Get the ProviderManager instance
|
|
2737
|
-
*/
|
|
2738
|
-
getProviderManager() {
|
|
2739
|
-
return this.providerManager;
|
|
2740
|
-
}
|
|
2741
|
-
/**
|
|
2742
|
-
* Check if the client is currently busy (in critical section)
|
|
2743
|
-
*/
|
|
2744
|
-
get isBusy() {
|
|
2745
|
-
return this.cashuSpender.isBusy;
|
|
2843
|
+
};
|
|
2844
|
+
|
|
2845
|
+
// storage/drivers/memory.ts
|
|
2846
|
+
var createMemoryDriver = (seed) => {
|
|
2847
|
+
const store = /* @__PURE__ */ new Map();
|
|
2848
|
+
if (seed) {
|
|
2849
|
+
for (const [key, value] of Object.entries(seed)) {
|
|
2850
|
+
store.set(key, value);
|
|
2851
|
+
}
|
|
2746
2852
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
method,
|
|
2759
|
-
body,
|
|
2760
|
-
headers = {},
|
|
2761
|
-
baseUrl,
|
|
2762
|
-
mintUrl,
|
|
2763
|
-
modelId
|
|
2764
|
-
} = params;
|
|
2765
|
-
await this._checkBalance();
|
|
2766
|
-
let requiredSats = 1;
|
|
2767
|
-
let selectedModel;
|
|
2768
|
-
if (modelId) {
|
|
2769
|
-
const providerModel = await this.providerManager.getModelForProvider(
|
|
2770
|
-
baseUrl,
|
|
2771
|
-
modelId
|
|
2772
|
-
);
|
|
2773
|
-
selectedModel = providerModel ?? void 0;
|
|
2774
|
-
if (selectedModel) {
|
|
2775
|
-
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
2776
|
-
selectedModel,
|
|
2777
|
-
[]
|
|
2778
|
-
);
|
|
2853
|
+
return {
|
|
2854
|
+
async getItem(key, defaultValue) {
|
|
2855
|
+
const item = store.get(key);
|
|
2856
|
+
if (item === void 0) return defaultValue;
|
|
2857
|
+
try {
|
|
2858
|
+
return JSON.parse(item);
|
|
2859
|
+
} catch (parseError) {
|
|
2860
|
+
if (typeof defaultValue === "string") {
|
|
2861
|
+
return item;
|
|
2862
|
+
}
|
|
2863
|
+
throw parseError;
|
|
2779
2864
|
}
|
|
2865
|
+
},
|
|
2866
|
+
async setItem(key, value) {
|
|
2867
|
+
store.set(key, JSON.stringify(value));
|
|
2868
|
+
},
|
|
2869
|
+
async removeItem(key) {
|
|
2870
|
+
store.delete(key);
|
|
2780
2871
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
}
|
|
2794
|
-
const baseHeaders = this._buildBaseHeaders(headers);
|
|
2795
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
2796
|
-
const response = await this._makeRequest({
|
|
2797
|
-
path,
|
|
2798
|
-
method,
|
|
2799
|
-
body: method === "GET" ? void 0 : requestBody,
|
|
2800
|
-
baseUrl,
|
|
2801
|
-
mintUrl,
|
|
2802
|
-
token,
|
|
2803
|
-
requiredSats,
|
|
2804
|
-
headers: requestHeaders,
|
|
2805
|
-
baseHeaders,
|
|
2806
|
-
selectedModel
|
|
2807
|
-
});
|
|
2808
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
2809
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
2810
|
-
const tokenUsed = response.token || token;
|
|
2811
|
-
await this._handlePostResponseBalanceUpdate({
|
|
2812
|
-
token: tokenUsed,
|
|
2813
|
-
baseUrl: baseUrlUsed,
|
|
2814
|
-
initialTokenBalance: tokenBalanceInSats,
|
|
2815
|
-
response
|
|
2816
|
-
});
|
|
2817
|
-
return response;
|
|
2872
|
+
};
|
|
2873
|
+
};
|
|
2874
|
+
|
|
2875
|
+
// storage/drivers/sqlite.ts
|
|
2876
|
+
var isBun = () => {
|
|
2877
|
+
return typeof process.versions.bun !== "undefined";
|
|
2878
|
+
};
|
|
2879
|
+
var createDatabase = (dbPath) => {
|
|
2880
|
+
if (isBun()) {
|
|
2881
|
+
throw new Error(
|
|
2882
|
+
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
2883
|
+
);
|
|
2818
2884
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
selectedModel,
|
|
2826
|
-
baseUrl,
|
|
2827
|
-
mintUrl,
|
|
2828
|
-
balance,
|
|
2829
|
-
transactionHistory,
|
|
2830
|
-
maxTokens,
|
|
2831
|
-
headers
|
|
2832
|
-
} = options;
|
|
2833
|
-
const apiMessages = await this._convertMessages(messageHistory);
|
|
2834
|
-
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
2835
|
-
selectedModel,
|
|
2836
|
-
apiMessages,
|
|
2837
|
-
maxTokens
|
|
2885
|
+
let Database = null;
|
|
2886
|
+
try {
|
|
2887
|
+
Database = __require("better-sqlite3");
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
throw new Error(
|
|
2890
|
+
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
2838
2891
|
);
|
|
2839
|
-
try {
|
|
2840
|
-
await this._checkBalance();
|
|
2841
|
-
callbacks.onPaymentProcessing?.(true);
|
|
2842
|
-
const spendResult = await this._spendToken({
|
|
2843
|
-
mintUrl,
|
|
2844
|
-
amount: requiredSats,
|
|
2845
|
-
baseUrl
|
|
2846
|
-
});
|
|
2847
|
-
let token = spendResult.token;
|
|
2848
|
-
let tokenBalance = spendResult.tokenBalance;
|
|
2849
|
-
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
2850
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
2851
|
-
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
2852
|
-
const baseHeaders = this._buildBaseHeaders(headers);
|
|
2853
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
2854
|
-
this.providerManager.resetFailedProviders();
|
|
2855
|
-
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
2856
|
-
const providerVersion = providerInfo?.version ?? "";
|
|
2857
|
-
let modelIdForRequest = selectedModel.id;
|
|
2858
|
-
if (/^0\.1\./.test(providerVersion)) {
|
|
2859
|
-
const newModel = await this.providerManager.getModelForProvider(
|
|
2860
|
-
baseUrl,
|
|
2861
|
-
selectedModel.id
|
|
2862
|
-
);
|
|
2863
|
-
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
2864
|
-
}
|
|
2865
|
-
const body = {
|
|
2866
|
-
model: modelIdForRequest,
|
|
2867
|
-
messages: apiMessages,
|
|
2868
|
-
stream: true
|
|
2869
|
-
};
|
|
2870
|
-
if (maxTokens !== void 0) {
|
|
2871
|
-
body.max_tokens = maxTokens;
|
|
2872
|
-
}
|
|
2873
|
-
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
2874
|
-
body.tools = [{ type: "web_search" }];
|
|
2875
|
-
}
|
|
2876
|
-
const response = await this._makeRequest({
|
|
2877
|
-
path: "/v1/chat/completions",
|
|
2878
|
-
method: "POST",
|
|
2879
|
-
body,
|
|
2880
|
-
selectedModel,
|
|
2881
|
-
baseUrl,
|
|
2882
|
-
mintUrl,
|
|
2883
|
-
token,
|
|
2884
|
-
requiredSats,
|
|
2885
|
-
maxTokens,
|
|
2886
|
-
headers: requestHeaders,
|
|
2887
|
-
baseHeaders
|
|
2888
|
-
});
|
|
2889
|
-
if (!response.body) {
|
|
2890
|
-
throw new Error("Response body is not available");
|
|
2891
|
-
}
|
|
2892
|
-
if (response.status === 200) {
|
|
2893
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
2894
|
-
const streamingResult = await this.streamProcessor.process(
|
|
2895
|
-
response,
|
|
2896
|
-
{
|
|
2897
|
-
onContent: callbacks.onStreamingUpdate,
|
|
2898
|
-
onThinking: callbacks.onThinkingUpdate
|
|
2899
|
-
},
|
|
2900
|
-
selectedModel.id
|
|
2901
|
-
);
|
|
2902
|
-
if (streamingResult.finish_reason === "content_filter") {
|
|
2903
|
-
callbacks.onMessageAppend({
|
|
2904
|
-
role: "assistant",
|
|
2905
|
-
content: "Your request was denied due to content filtering."
|
|
2906
|
-
});
|
|
2907
|
-
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
2908
|
-
const message = await this._createAssistantMessage(streamingResult);
|
|
2909
|
-
callbacks.onMessageAppend(message);
|
|
2910
|
-
} else {
|
|
2911
|
-
callbacks.onMessageAppend({
|
|
2912
|
-
role: "system",
|
|
2913
|
-
content: "The provider did not respond to this request."
|
|
2914
|
-
});
|
|
2915
|
-
}
|
|
2916
|
-
callbacks.onStreamingUpdate("");
|
|
2917
|
-
callbacks.onThinkingUpdate("");
|
|
2918
|
-
const isApikeysEstimate = this.mode === "apikeys";
|
|
2919
|
-
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
2920
|
-
token,
|
|
2921
|
-
baseUrl: baseUrlUsed,
|
|
2922
|
-
initialTokenBalance: tokenBalanceInSats,
|
|
2923
|
-
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
2924
|
-
response
|
|
2925
|
-
});
|
|
2926
|
-
const estimatedCosts = this._getEstimatedCosts(
|
|
2927
|
-
selectedModel,
|
|
2928
|
-
streamingResult
|
|
2929
|
-
);
|
|
2930
|
-
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
2931
|
-
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
2932
|
-
} else {
|
|
2933
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
2934
|
-
}
|
|
2935
|
-
} catch (error) {
|
|
2936
|
-
this._handleError(error, callbacks);
|
|
2937
|
-
} finally {
|
|
2938
|
-
callbacks.onPaymentProcessing?.(false);
|
|
2939
|
-
}
|
|
2940
2892
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
const
|
|
2960
|
-
|
|
2893
|
+
return new Database(dbPath);
|
|
2894
|
+
};
|
|
2895
|
+
var createSqliteDriver = (options = {}) => {
|
|
2896
|
+
const dbPath = options.dbPath || "routstr.sqlite";
|
|
2897
|
+
const tableName = options.tableName || "sdk_storage";
|
|
2898
|
+
const db = createDatabase(dbPath);
|
|
2899
|
+
db.exec(
|
|
2900
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
2901
|
+
);
|
|
2902
|
+
const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
2903
|
+
const upsertStmt = db.prepare(
|
|
2904
|
+
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
2905
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
2906
|
+
);
|
|
2907
|
+
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
2908
|
+
return {
|
|
2909
|
+
async getItem(key, defaultValue) {
|
|
2910
|
+
try {
|
|
2911
|
+
const row = selectStmt.get(key);
|
|
2912
|
+
if (!row || typeof row.value !== "string") return defaultValue;
|
|
2961
2913
|
try {
|
|
2962
|
-
|
|
2963
|
-
} catch (
|
|
2964
|
-
|
|
2914
|
+
return JSON.parse(row.value);
|
|
2915
|
+
} catch (parseError) {
|
|
2916
|
+
if (typeof defaultValue === "string") {
|
|
2917
|
+
return row.value;
|
|
2918
|
+
}
|
|
2919
|
+
throw parseError;
|
|
2965
2920
|
}
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
response.status,
|
|
2970
|
-
requestId,
|
|
2971
|
-
this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
|
|
2972
|
-
bodyText,
|
|
2973
|
-
params.retryCount ?? 0
|
|
2974
|
-
);
|
|
2921
|
+
} catch (error) {
|
|
2922
|
+
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
2923
|
+
return defaultValue;
|
|
2975
2924
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
);
|
|
2925
|
+
},
|
|
2926
|
+
async setItem(key, value) {
|
|
2927
|
+
try {
|
|
2928
|
+
upsertStmt.run(key, JSON.stringify(value));
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
async removeItem(key) {
|
|
2934
|
+
try {
|
|
2935
|
+
deleteStmt.run(key);
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
console.error(`SQLite removeItem failed for key "${key}":`, error);
|
|
2989
2938
|
}
|
|
2990
|
-
throw error;
|
|
2991
2939
|
}
|
|
2940
|
+
};
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
// storage/drivers/indexedDB.ts
|
|
2944
|
+
var isBrowser = typeof indexedDB !== "undefined";
|
|
2945
|
+
var openDatabase = (dbName, storeName) => {
|
|
2946
|
+
if (!isBrowser) {
|
|
2947
|
+
return Promise.reject(new Error("IndexedDB is not available"));
|
|
2992
2948
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
let tryNextProvider = false;
|
|
3000
|
-
this._log(
|
|
3001
|
-
"DEBUG",
|
|
3002
|
-
`[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
|
|
3003
|
-
);
|
|
3004
|
-
this._log(
|
|
3005
|
-
"DEBUG",
|
|
3006
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
|
|
3007
|
-
);
|
|
3008
|
-
if (params.token.startsWith("cashu")) {
|
|
3009
|
-
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
3010
|
-
params.token
|
|
3011
|
-
);
|
|
3012
|
-
if (tryReceiveTokenResult.success) {
|
|
3013
|
-
this._log(
|
|
3014
|
-
"DEBUG",
|
|
3015
|
-
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
3016
|
-
);
|
|
3017
|
-
tryNextProvider = true;
|
|
3018
|
-
if (this.mode === "lazyrefund")
|
|
3019
|
-
this.storageAdapter.removeToken(baseUrl);
|
|
3020
|
-
} else {
|
|
3021
|
-
this._log(
|
|
3022
|
-
"DEBUG",
|
|
3023
|
-
`[RoutstrClient] _handleErrorResponse: Failed to receive token. `
|
|
3024
|
-
);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
if (this.mode === "xcashu") {
|
|
3028
|
-
if (xCashuRefundToken) {
|
|
3029
|
-
this._log(
|
|
3030
|
-
"DEBUG",
|
|
3031
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
|
|
3032
|
-
);
|
|
3033
|
-
try {
|
|
3034
|
-
const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
|
|
3035
|
-
if (receiveResult.success) {
|
|
3036
|
-
this._log(
|
|
3037
|
-
"DEBUG",
|
|
3038
|
-
`[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
|
|
3039
|
-
);
|
|
3040
|
-
tryNextProvider = true;
|
|
3041
|
-
} else
|
|
3042
|
-
throw new ProviderError(
|
|
3043
|
-
baseUrl,
|
|
3044
|
-
status,
|
|
3045
|
-
"xcashu refund failed",
|
|
3046
|
-
requestId
|
|
3047
|
-
);
|
|
3048
|
-
} catch (error) {
|
|
3049
|
-
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
3050
|
-
throw new ProviderError(
|
|
3051
|
-
baseUrl,
|
|
3052
|
-
status,
|
|
3053
|
-
"[xcashu] Failed to receive refund token",
|
|
3054
|
-
requestId
|
|
3055
|
-
);
|
|
3056
|
-
}
|
|
3057
|
-
} else {
|
|
3058
|
-
if (!tryNextProvider)
|
|
3059
|
-
throw new ProviderError(
|
|
3060
|
-
baseUrl,
|
|
3061
|
-
status,
|
|
3062
|
-
"[xcashu] Failed to receive refund token",
|
|
3063
|
-
requestId
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
|
|
3068
|
-
this.storageAdapter.getApiKey(baseUrl);
|
|
3069
|
-
let topupAmount = params.requiredSats;
|
|
3070
|
-
try {
|
|
3071
|
-
let currentBalance = 0;
|
|
3072
|
-
if (this.mode === "apikeys") {
|
|
3073
|
-
const currentBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3074
|
-
params.token,
|
|
3075
|
-
baseUrl
|
|
3076
|
-
);
|
|
3077
|
-
currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
|
|
3078
|
-
} else if (this.mode === "lazyrefund") {
|
|
3079
|
-
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
3080
|
-
const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
|
|
3081
|
-
currentBalance = tokenEntry?.amount ?? 0;
|
|
3082
|
-
}
|
|
3083
|
-
const shortfall = Math.max(0, params.requiredSats - currentBalance);
|
|
3084
|
-
topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
|
|
3085
|
-
} catch (e) {
|
|
3086
|
-
this._log(
|
|
3087
|
-
"WARN",
|
|
3088
|
-
"Could not get current token balance for topup calculation:",
|
|
3089
|
-
e
|
|
3090
|
-
);
|
|
3091
|
-
}
|
|
3092
|
-
const topupResult = await this.balanceManager.topUp({
|
|
3093
|
-
mintUrl,
|
|
3094
|
-
baseUrl,
|
|
3095
|
-
amount: topupAmount * TOPUP_MARGIN,
|
|
3096
|
-
token: params.token
|
|
3097
|
-
});
|
|
3098
|
-
this._log(
|
|
3099
|
-
"DEBUG",
|
|
3100
|
-
`[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
|
|
3101
|
-
);
|
|
3102
|
-
if (!topupResult.success) {
|
|
3103
|
-
const message = topupResult.message || "";
|
|
3104
|
-
if (message.includes("Insufficient balance")) {
|
|
3105
|
-
const needMatch = message.match(/need (\d+)/);
|
|
3106
|
-
const haveMatch = message.match(/have (\d+)/);
|
|
3107
|
-
const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
|
|
3108
|
-
const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
|
|
3109
|
-
this._log(
|
|
3110
|
-
"DEBUG",
|
|
3111
|
-
`[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
|
|
3112
|
-
);
|
|
3113
|
-
throw new InsufficientBalanceError(
|
|
3114
|
-
required,
|
|
3115
|
-
available,
|
|
3116
|
-
0,
|
|
3117
|
-
"",
|
|
3118
|
-
message
|
|
3119
|
-
);
|
|
3120
|
-
} else {
|
|
3121
|
-
this._log(
|
|
3122
|
-
"DEBUG",
|
|
3123
|
-
`[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
|
|
3124
|
-
);
|
|
3125
|
-
tryNextProvider = true;
|
|
3126
|
-
}
|
|
3127
|
-
} else {
|
|
3128
|
-
this._log(
|
|
3129
|
-
"DEBUG",
|
|
3130
|
-
`[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
|
|
3131
|
-
);
|
|
3132
|
-
}
|
|
3133
|
-
if (!tryNextProvider) {
|
|
3134
|
-
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
3135
|
-
this._log(
|
|
3136
|
-
"DEBUG",
|
|
3137
|
-
`[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
3138
|
-
);
|
|
3139
|
-
return this._makeRequest({
|
|
3140
|
-
...params,
|
|
3141
|
-
token: params.token,
|
|
3142
|
-
headers: this._withAuthHeader(params.baseHeaders, params.token),
|
|
3143
|
-
retryCount: retryCount + 1
|
|
3144
|
-
});
|
|
3145
|
-
} else {
|
|
3146
|
-
this._log(
|
|
3147
|
-
"DEBUG",
|
|
3148
|
-
`[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
3149
|
-
);
|
|
3150
|
-
tryNextProvider = true;
|
|
3151
|
-
}
|
|
2949
|
+
return new Promise((resolve, reject) => {
|
|
2950
|
+
const request = indexedDB.open(dbName, 1);
|
|
2951
|
+
request.onupgradeneeded = () => {
|
|
2952
|
+
const db = request.result;
|
|
2953
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
2954
|
+
db.createObjectStore(storeName);
|
|
3152
2955
|
}
|
|
2956
|
+
};
|
|
2957
|
+
request.onsuccess = () => resolve(request.result);
|
|
2958
|
+
request.onerror = () => reject(request.error);
|
|
2959
|
+
});
|
|
2960
|
+
};
|
|
2961
|
+
var createIndexedDBDriver = (options = {}) => {
|
|
2962
|
+
const dbName = options.dbName || "routstr-sdk";
|
|
2963
|
+
const storeName = options.storeName || "sdk_storage";
|
|
2964
|
+
let dbPromise = null;
|
|
2965
|
+
const getDb = () => {
|
|
2966
|
+
if (!dbPromise) {
|
|
2967
|
+
dbPromise = openDatabase(dbName, storeName);
|
|
3153
2968
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
2969
|
+
return dbPromise;
|
|
2970
|
+
};
|
|
2971
|
+
return {
|
|
2972
|
+
async getItem(key, defaultValue) {
|
|
3157
2973
|
try {
|
|
3158
|
-
const
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
2974
|
+
const db = await getDb();
|
|
2975
|
+
return new Promise((resolve, reject) => {
|
|
2976
|
+
const tx = db.transaction(storeName, "readonly");
|
|
2977
|
+
const store = tx.objectStore(storeName);
|
|
2978
|
+
const request = store.get(key);
|
|
2979
|
+
request.onsuccess = () => {
|
|
2980
|
+
const raw = request.result;
|
|
2981
|
+
if (raw === void 0) {
|
|
2982
|
+
resolve(defaultValue);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
if (typeof raw === "string") {
|
|
2986
|
+
try {
|
|
2987
|
+
resolve(JSON.parse(raw));
|
|
2988
|
+
} catch {
|
|
2989
|
+
if (typeof defaultValue === "string") {
|
|
2990
|
+
resolve(raw);
|
|
2991
|
+
} else {
|
|
2992
|
+
resolve(defaultValue);
|
|
2993
|
+
}
|
|
3176
2994
|
}
|
|
3177
|
-
|
|
2995
|
+
} else {
|
|
2996
|
+
resolve(raw);
|
|
3178
2997
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
this.storageAdapter.updateApiKeyBalance(
|
|
3183
|
-
baseUrl,
|
|
3184
|
-
latestTokenBalance
|
|
3185
|
-
);
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
2998
|
+
};
|
|
2999
|
+
request.onerror = () => reject(request.error);
|
|
3000
|
+
});
|
|
3188
3001
|
} catch (error) {
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
`[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
|
|
3192
|
-
error
|
|
3193
|
-
);
|
|
3002
|
+
console.error(`IndexedDB getItem failed for key "${key}":`, error);
|
|
3003
|
+
return defaultValue;
|
|
3194
3004
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
)
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3005
|
+
},
|
|
3006
|
+
async setItem(key, value) {
|
|
3007
|
+
try {
|
|
3008
|
+
const db = await getDb();
|
|
3009
|
+
return new Promise((resolve, reject) => {
|
|
3010
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3011
|
+
const store = tx.objectStore(storeName);
|
|
3012
|
+
store.put(JSON.stringify(value), key);
|
|
3013
|
+
tx.oncomplete = () => resolve();
|
|
3014
|
+
tx.onerror = () => reject(tx.error);
|
|
3205
3015
|
});
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
console.error(`IndexedDB setItem failed for key "${key}":`, error);
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
async removeItem(key) {
|
|
3021
|
+
try {
|
|
3022
|
+
const db = await getDb();
|
|
3023
|
+
return new Promise((resolve, reject) => {
|
|
3024
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3025
|
+
const store = tx.objectStore(storeName);
|
|
3026
|
+
store.delete(key);
|
|
3027
|
+
tx.oncomplete = () => resolve();
|
|
3028
|
+
tx.onerror = () => reject(tx.error);
|
|
3029
|
+
});
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
console.error(`IndexedDB removeItem failed for key "${key}":`, error);
|
|
3212
3032
|
}
|
|
3213
3033
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
);
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3034
|
+
};
|
|
3035
|
+
};
|
|
3036
|
+
|
|
3037
|
+
// storage/keys.ts
|
|
3038
|
+
var SDK_STORAGE_KEYS = {
|
|
3039
|
+
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
3040
|
+
LAST_USED_MODEL: "lastUsedModel",
|
|
3041
|
+
BASE_URLS_LIST: "base_urls_list",
|
|
3042
|
+
DISABLED_PROVIDERS: "disabled_providers",
|
|
3043
|
+
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
3044
|
+
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
3045
|
+
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
3046
|
+
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
3047
|
+
LOCAL_CASHU_TOKENS: "local_cashu_tokens",
|
|
3048
|
+
API_KEYS: "api_keys",
|
|
3049
|
+
CHILD_KEYS: "child_keys",
|
|
3050
|
+
ROUTSTR21_MODELS: "routstr21Models",
|
|
3051
|
+
LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
|
|
3052
|
+
CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
|
|
3053
|
+
USAGE_TRACKING: "usage_tracking",
|
|
3054
|
+
CLIENT_IDS: "client_ids"
|
|
3055
|
+
};
|
|
3056
|
+
|
|
3057
|
+
// storage/usageTracking/indexedDB.ts
|
|
3058
|
+
var DEFAULT_DB_NAME = "routstr-sdk";
|
|
3059
|
+
var DEFAULT_STORE_NAME = "usage_tracking";
|
|
3060
|
+
var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
|
|
3061
|
+
var isBrowser2 = typeof indexedDB !== "undefined";
|
|
3062
|
+
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3063
|
+
var openDatabase2 = (dbName, storeName) => {
|
|
3064
|
+
if (!isBrowser2) {
|
|
3065
|
+
return Promise.reject(new Error("IndexedDB is not available"));
|
|
3066
|
+
}
|
|
3067
|
+
return new Promise((resolve, reject) => {
|
|
3068
|
+
const request = indexedDB.open(dbName, 1);
|
|
3069
|
+
request.onupgradeneeded = () => {
|
|
3070
|
+
const db = request.result;
|
|
3071
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
3072
|
+
const store = db.createObjectStore(storeName, { keyPath: "id" });
|
|
3073
|
+
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
3074
|
+
store.createIndex("modelId", "modelId", { unique: false });
|
|
3075
|
+
store.createIndex("baseUrl", "baseUrl", { unique: false });
|
|
3076
|
+
store.createIndex("sessionId", "sessionId", { unique: false });
|
|
3077
|
+
store.createIndex("client", "client", { unique: false });
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
request.onsuccess = () => resolve(request.result);
|
|
3081
|
+
request.onerror = () => reject(request.error);
|
|
3082
|
+
});
|
|
3083
|
+
};
|
|
3084
|
+
var matchesFilters = (entry, options = {}) => {
|
|
3085
|
+
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
3086
|
+
return false;
|
|
3087
|
+
}
|
|
3088
|
+
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
if (options.modelId && entry.modelId !== options.modelId) {
|
|
3092
|
+
return false;
|
|
3093
|
+
}
|
|
3094
|
+
if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
3098
|
+
return false;
|
|
3099
|
+
}
|
|
3100
|
+
if (options.client && entry.client !== options.client) {
|
|
3101
|
+
return false;
|
|
3102
|
+
}
|
|
3103
|
+
return true;
|
|
3104
|
+
};
|
|
3105
|
+
var createIndexedDBUsageTrackingDriver = (options = {}) => {
|
|
3106
|
+
const dbName = options.dbName || DEFAULT_DB_NAME;
|
|
3107
|
+
const storeName = options.storeName || DEFAULT_STORE_NAME;
|
|
3108
|
+
const legacyStorageDriver = options.legacyStorageDriver;
|
|
3109
|
+
let dbPromise = null;
|
|
3110
|
+
let migrationPromise = null;
|
|
3111
|
+
const getDb = () => {
|
|
3112
|
+
if (!dbPromise) {
|
|
3113
|
+
dbPromise = openDatabase2(dbName, storeName);
|
|
3114
|
+
}
|
|
3115
|
+
return dbPromise;
|
|
3116
|
+
};
|
|
3117
|
+
const putMany = async (entries) => {
|
|
3118
|
+
if (entries.length === 0) return;
|
|
3119
|
+
const db = await getDb();
|
|
3120
|
+
await new Promise((resolve, reject) => {
|
|
3121
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3122
|
+
const store = tx.objectStore(storeName);
|
|
3123
|
+
for (const entry of entries) {
|
|
3124
|
+
store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
|
|
3125
|
+
}
|
|
3126
|
+
tx.oncomplete = () => resolve();
|
|
3127
|
+
tx.onerror = () => reject(tx.error);
|
|
3128
|
+
});
|
|
3129
|
+
};
|
|
3130
|
+
const ensureMigrated = async () => {
|
|
3131
|
+
if (!legacyStorageDriver) return;
|
|
3132
|
+
if (!migrationPromise) {
|
|
3133
|
+
migrationPromise = (async () => {
|
|
3134
|
+
const migrated = await legacyStorageDriver.getItem(
|
|
3135
|
+
MIGRATION_MARKER_KEY,
|
|
3136
|
+
false
|
|
3258
3137
|
);
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
});
|
|
3264
|
-
this._log(
|
|
3265
|
-
"DEBUG",
|
|
3266
|
-
`[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
|
|
3138
|
+
if (migrated) return;
|
|
3139
|
+
const legacyEntries = await legacyStorageDriver.getItem(
|
|
3140
|
+
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
3141
|
+
[]
|
|
3267
3142
|
);
|
|
3268
|
-
if (
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
status,
|
|
3272
|
-
refundResult.message ?? "Unknown error"
|
|
3273
|
-
);
|
|
3143
|
+
if (legacyEntries.length > 0) {
|
|
3144
|
+
await putMany(legacyEntries);
|
|
3145
|
+
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
3274
3146
|
}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
this.providerManager.markFailed(baseUrl);
|
|
3278
|
-
this._log(
|
|
3279
|
-
"DEBUG",
|
|
3280
|
-
`[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
|
|
3281
|
-
);
|
|
3282
|
-
if (!selectedModel) {
|
|
3283
|
-
throw new ProviderError(
|
|
3284
|
-
baseUrl,
|
|
3285
|
-
status,
|
|
3286
|
-
"Funny, no selected model. HMM. "
|
|
3287
|
-
);
|
|
3147
|
+
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
|
|
3148
|
+
})();
|
|
3288
3149
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
)
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
);
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
)
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
)
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3150
|
+
await migrationPromise;
|
|
3151
|
+
};
|
|
3152
|
+
return {
|
|
3153
|
+
async migrate() {
|
|
3154
|
+
await ensureMigrated();
|
|
3155
|
+
},
|
|
3156
|
+
async append(entry) {
|
|
3157
|
+
await ensureMigrated();
|
|
3158
|
+
await putMany([entry]);
|
|
3159
|
+
},
|
|
3160
|
+
async appendMany(entries) {
|
|
3161
|
+
await ensureMigrated();
|
|
3162
|
+
await putMany(entries);
|
|
3163
|
+
},
|
|
3164
|
+
async list(options2 = {}) {
|
|
3165
|
+
await ensureMigrated();
|
|
3166
|
+
const db = await getDb();
|
|
3167
|
+
return new Promise((resolve, reject) => {
|
|
3168
|
+
const tx = db.transaction(storeName, "readonly");
|
|
3169
|
+
const store = tx.objectStore(storeName);
|
|
3170
|
+
const index = store.index("timestamp");
|
|
3171
|
+
const direction = "prev";
|
|
3172
|
+
const request = index.openCursor(null, direction);
|
|
3173
|
+
const results = [];
|
|
3174
|
+
const limit = options2.limit;
|
|
3175
|
+
request.onsuccess = () => {
|
|
3176
|
+
const cursor = request.result;
|
|
3177
|
+
if (!cursor) {
|
|
3178
|
+
resolve(results);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const value = cursor.value;
|
|
3182
|
+
if (matchesFilters(value, options2)) {
|
|
3183
|
+
results.push(value);
|
|
3184
|
+
if (typeof limit === "number" && results.length >= limit) {
|
|
3185
|
+
resolve(results);
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
cursor.continue();
|
|
3190
|
+
};
|
|
3191
|
+
request.onerror = () => reject(request.error);
|
|
3318
3192
|
});
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3193
|
+
},
|
|
3194
|
+
async count(options2 = {}) {
|
|
3195
|
+
const results = await this.list(options2);
|
|
3196
|
+
return results.length;
|
|
3197
|
+
},
|
|
3198
|
+
async deleteOlderThan(timestamp) {
|
|
3199
|
+
await ensureMigrated();
|
|
3200
|
+
const db = await getDb();
|
|
3201
|
+
return new Promise((resolve, reject) => {
|
|
3202
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3203
|
+
const store = tx.objectStore(storeName);
|
|
3204
|
+
const index = store.index("timestamp");
|
|
3205
|
+
const range = IDBKeyRange.upperBound(timestamp, true);
|
|
3206
|
+
const request = index.openCursor(range);
|
|
3207
|
+
let deleted = 0;
|
|
3208
|
+
request.onsuccess = () => {
|
|
3209
|
+
const cursor = request.result;
|
|
3210
|
+
if (!cursor) {
|
|
3211
|
+
resolve(deleted);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
deleted += 1;
|
|
3215
|
+
cursor.delete();
|
|
3216
|
+
cursor.continue();
|
|
3217
|
+
};
|
|
3218
|
+
request.onerror = () => reject(request.error);
|
|
3219
|
+
});
|
|
3220
|
+
},
|
|
3221
|
+
async clear() {
|
|
3222
|
+
await ensureMigrated();
|
|
3223
|
+
const db = await getDb();
|
|
3224
|
+
await new Promise((resolve, reject) => {
|
|
3225
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3226
|
+
tx.objectStore(storeName).clear();
|
|
3227
|
+
tx.oncomplete = () => resolve();
|
|
3228
|
+
tx.onerror = () => reject(tx.error);
|
|
3330
3229
|
});
|
|
3331
3230
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3231
|
+
};
|
|
3232
|
+
};
|
|
3233
|
+
|
|
3234
|
+
// storage/usageTracking/sqlite.ts
|
|
3235
|
+
var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
|
|
3236
|
+
var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3237
|
+
var isBun2 = () => {
|
|
3238
|
+
return typeof process.versions.bun !== "undefined";
|
|
3239
|
+
};
|
|
3240
|
+
var createDatabase2 = (dbPath) => {
|
|
3241
|
+
if (isBun2()) {
|
|
3242
|
+
throw new Error(
|
|
3243
|
+
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
3335
3244
|
);
|
|
3336
3245
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
if (this.mode === "xcashu" && response) {
|
|
3344
|
-
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
3345
|
-
if (refundToken) {
|
|
3346
|
-
try {
|
|
3347
|
-
const receiveResult = await this.cashuSpender.receiveToken(refundToken);
|
|
3348
|
-
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
3349
|
-
} catch (error) {
|
|
3350
|
-
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
} else if (this.mode === "lazyrefund") {
|
|
3354
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3355
|
-
token,
|
|
3356
|
-
baseUrl
|
|
3357
|
-
);
|
|
3358
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3359
|
-
this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
|
|
3360
|
-
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3361
|
-
} else if (this.mode === "apikeys") {
|
|
3362
|
-
try {
|
|
3363
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3364
|
-
token,
|
|
3365
|
-
baseUrl
|
|
3366
|
-
);
|
|
3367
|
-
this._log(
|
|
3368
|
-
"DEBUG",
|
|
3369
|
-
"LATEST Balance",
|
|
3370
|
-
latestBalanceInfo.amount,
|
|
3371
|
-
latestBalanceInfo.reserved,
|
|
3372
|
-
latestBalanceInfo.apiKey,
|
|
3373
|
-
baseUrl
|
|
3374
|
-
);
|
|
3375
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3376
|
-
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
3377
|
-
if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
|
|
3378
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
3379
|
-
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
3380
|
-
}
|
|
3381
|
-
this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
|
|
3382
|
-
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3383
|
-
} catch (e) {
|
|
3384
|
-
this._log("WARN", "Could not get updated API key balance:", e);
|
|
3385
|
-
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
return satsSpent;
|
|
3389
|
-
}
|
|
3390
|
-
/**
|
|
3391
|
-
* Convert messages for API format
|
|
3392
|
-
*/
|
|
3393
|
-
async _convertMessages(messages) {
|
|
3394
|
-
return Promise.all(
|
|
3395
|
-
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
3396
|
-
role: m.role,
|
|
3397
|
-
content: typeof m.content === "string" ? m.content : m.content
|
|
3398
|
-
}))
|
|
3246
|
+
let Database = null;
|
|
3247
|
+
try {
|
|
3248
|
+
Database = __require("better-sqlite3");
|
|
3249
|
+
} catch (error) {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
`better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
|
|
3399
3252
|
);
|
|
3400
3253
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
type: "text",
|
|
3410
|
-
text: result.content,
|
|
3411
|
-
thinking: result.thinking,
|
|
3412
|
-
citations: result.citations,
|
|
3413
|
-
annotations: result.annotations
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
for (const img of result.images) {
|
|
3417
|
-
content.push({
|
|
3418
|
-
type: "image_url",
|
|
3419
|
-
image_url: {
|
|
3420
|
-
url: img.image_url.url
|
|
3421
|
-
}
|
|
3422
|
-
});
|
|
3423
|
-
}
|
|
3424
|
-
return {
|
|
3425
|
-
role: "assistant",
|
|
3426
|
-
content
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
return {
|
|
3430
|
-
role: "assistant",
|
|
3431
|
-
content: result.content || ""
|
|
3432
|
-
};
|
|
3254
|
+
return new Database(dbPath);
|
|
3255
|
+
};
|
|
3256
|
+
var buildWhereClause = (options = {}) => {
|
|
3257
|
+
const clauses = [];
|
|
3258
|
+
const params = [];
|
|
3259
|
+
if (typeof options.before === "number") {
|
|
3260
|
+
clauses.push("timestamp < ?");
|
|
3261
|
+
params.push(options.before);
|
|
3433
3262
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
*/
|
|
3438
|
-
async _createChildKey(baseUrl, parentApiKey, options) {
|
|
3439
|
-
const response = await fetch(`${baseUrl}v1/balance/child-key`, {
|
|
3440
|
-
method: "POST",
|
|
3441
|
-
headers: {
|
|
3442
|
-
"Content-Type": "application/json",
|
|
3443
|
-
Authorization: `Bearer ${parentApiKey}`
|
|
3444
|
-
},
|
|
3445
|
-
body: JSON.stringify({
|
|
3446
|
-
count: options?.count ?? 1,
|
|
3447
|
-
balance_limit: options?.balanceLimit,
|
|
3448
|
-
balance_limit_reset: options?.balanceLimitReset,
|
|
3449
|
-
validity_date: options?.validityDate
|
|
3450
|
-
})
|
|
3451
|
-
});
|
|
3452
|
-
if (!response.ok) {
|
|
3453
|
-
throw new Error(
|
|
3454
|
-
`Failed to create child key: ${response.status} ${await response.text()}`
|
|
3455
|
-
);
|
|
3456
|
-
}
|
|
3457
|
-
const data = await response.json();
|
|
3458
|
-
return {
|
|
3459
|
-
childKey: data.api_keys?.[0],
|
|
3460
|
-
balance: data.balance ?? 0,
|
|
3461
|
-
balanceLimit: data.balance_limit,
|
|
3462
|
-
validityDate: data.validity_date
|
|
3463
|
-
};
|
|
3263
|
+
if (typeof options.after === "number") {
|
|
3264
|
+
clauses.push("timestamp > ?");
|
|
3265
|
+
params.push(options.after);
|
|
3464
3266
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
3469
|
-
let estimatedCosts = 0;
|
|
3470
|
-
if (streamingResult.usage) {
|
|
3471
|
-
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
3472
|
-
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
3473
|
-
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
|
-
return estimatedCosts;
|
|
3267
|
+
if (options.modelId) {
|
|
3268
|
+
clauses.push("model_id = ?");
|
|
3269
|
+
params.push(options.modelId);
|
|
3477
3270
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
_getPendingCashuTokenAmount() {
|
|
3482
|
-
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
3483
|
-
return distribution.reduce((total, item) => total + item.amount, 0);
|
|
3271
|
+
if (options.baseUrl) {
|
|
3272
|
+
clauses.push("base_url = ?");
|
|
3273
|
+
params.push(normalizeBaseUrl2(options.baseUrl));
|
|
3484
3274
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
_handleError(error, callbacks) {
|
|
3489
|
-
this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
|
|
3490
|
-
if (error instanceof Error) {
|
|
3491
|
-
const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
|
|
3492
|
-
const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
|
|
3493
|
-
this._log(
|
|
3494
|
-
"ERROR",
|
|
3495
|
-
`[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
|
|
3496
|
-
);
|
|
3497
|
-
callbacks.onMessageAppend({
|
|
3498
|
-
role: "system",
|
|
3499
|
-
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
3500
|
-
});
|
|
3501
|
-
} else {
|
|
3502
|
-
callbacks.onMessageAppend({
|
|
3503
|
-
role: "system",
|
|
3504
|
-
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
3505
|
-
});
|
|
3506
|
-
}
|
|
3275
|
+
if (options.sessionId) {
|
|
3276
|
+
clauses.push("session_id = ?");
|
|
3277
|
+
params.push(options.sessionId);
|
|
3507
3278
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
async _checkBalance() {
|
|
3512
|
-
const balances = await this.walletAdapter.getBalances();
|
|
3513
|
-
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
3514
|
-
if (totalBalance <= 0) {
|
|
3515
|
-
throw new InsufficientBalanceError(1, 0);
|
|
3516
|
-
}
|
|
3279
|
+
if (options.client) {
|
|
3280
|
+
clauses.push("client = ?");
|
|
3281
|
+
params.push(options.client);
|
|
3517
3282
|
}
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3283
|
+
return {
|
|
3284
|
+
sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
3285
|
+
params
|
|
3286
|
+
};
|
|
3287
|
+
};
|
|
3288
|
+
var createSqliteUsageTrackingDriver = (options = {}) => {
|
|
3289
|
+
const dbPath = options.dbPath || "routstr.sqlite";
|
|
3290
|
+
const tableName = options.tableName || "usage_tracking";
|
|
3291
|
+
const db = createDatabase2(dbPath);
|
|
3292
|
+
const legacyStorageDriver = options.legacyStorageDriver;
|
|
3293
|
+
db.exec(`
|
|
3294
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3295
|
+
id TEXT PRIMARY KEY,
|
|
3296
|
+
timestamp INTEGER NOT NULL,
|
|
3297
|
+
model_id TEXT NOT NULL,
|
|
3298
|
+
base_url TEXT NOT NULL,
|
|
3299
|
+
request_id TEXT NOT NULL,
|
|
3300
|
+
cost REAL NOT NULL,
|
|
3301
|
+
sats_cost REAL NOT NULL,
|
|
3302
|
+
prompt_tokens INTEGER NOT NULL,
|
|
3303
|
+
completion_tokens INTEGER NOT NULL,
|
|
3304
|
+
total_tokens INTEGER NOT NULL,
|
|
3305
|
+
client TEXT,
|
|
3306
|
+
session_id TEXT,
|
|
3307
|
+
tags TEXT
|
|
3526
3308
|
);
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3309
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
|
|
3310
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
|
|
3311
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
|
|
3312
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
|
|
3313
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
|
|
3314
|
+
`);
|
|
3315
|
+
const insertStmt = db.prepare(`
|
|
3316
|
+
INSERT OR REPLACE INTO ${tableName} (
|
|
3317
|
+
id, timestamp, model_id, base_url, request_id,
|
|
3318
|
+
cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
|
|
3319
|
+
client, session_id, tags
|
|
3320
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3321
|
+
`);
|
|
3322
|
+
let migrationComplete = false;
|
|
3323
|
+
const appendOne = (entry) => {
|
|
3324
|
+
insertStmt.run(
|
|
3325
|
+
entry.id,
|
|
3326
|
+
entry.timestamp,
|
|
3327
|
+
entry.modelId,
|
|
3328
|
+
normalizeBaseUrl2(entry.baseUrl),
|
|
3329
|
+
entry.requestId,
|
|
3330
|
+
entry.cost,
|
|
3331
|
+
entry.satsCost,
|
|
3332
|
+
entry.promptTokens,
|
|
3333
|
+
entry.completionTokens,
|
|
3334
|
+
entry.totalTokens,
|
|
3335
|
+
entry.client ?? null,
|
|
3336
|
+
entry.sessionId ?? null,
|
|
3337
|
+
JSON.stringify(entry.tags ?? [])
|
|
3338
|
+
);
|
|
3339
|
+
};
|
|
3340
|
+
const ensureMigrated = async () => {
|
|
3341
|
+
if (!legacyStorageDriver || migrationComplete) return;
|
|
3342
|
+
const migrated = await legacyStorageDriver.getItem(
|
|
3343
|
+
MIGRATION_MARKER_KEY2,
|
|
3344
|
+
false
|
|
3345
|
+
);
|
|
3346
|
+
if (migrated) {
|
|
3347
|
+
migrationComplete = true;
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
const legacyEntries = await legacyStorageDriver.getItem(
|
|
3351
|
+
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
3352
|
+
[]
|
|
3353
|
+
);
|
|
3354
|
+
for (const entry of legacyEntries) {
|
|
3355
|
+
appendOne(entry);
|
|
3356
|
+
}
|
|
3357
|
+
if (legacyEntries.length > 0) {
|
|
3358
|
+
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
3359
|
+
}
|
|
3360
|
+
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
|
|
3361
|
+
migrationComplete = true;
|
|
3362
|
+
};
|
|
3363
|
+
const mapRow = (row) => ({
|
|
3364
|
+
id: row.id,
|
|
3365
|
+
timestamp: row.timestamp,
|
|
3366
|
+
modelId: row.model_id,
|
|
3367
|
+
baseUrl: row.base_url,
|
|
3368
|
+
requestId: row.request_id,
|
|
3369
|
+
cost: row.cost,
|
|
3370
|
+
satsCost: row.sats_cost,
|
|
3371
|
+
promptTokens: row.prompt_tokens,
|
|
3372
|
+
completionTokens: row.completion_tokens,
|
|
3373
|
+
totalTokens: row.total_tokens,
|
|
3374
|
+
client: row.client ?? void 0,
|
|
3375
|
+
sessionId: row.session_id ?? void 0,
|
|
3376
|
+
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
|
|
3377
|
+
});
|
|
3378
|
+
return {
|
|
3379
|
+
async migrate() {
|
|
3380
|
+
await ensureMigrated();
|
|
3381
|
+
},
|
|
3382
|
+
async append(entry) {
|
|
3383
|
+
await ensureMigrated();
|
|
3384
|
+
appendOne(entry);
|
|
3385
|
+
},
|
|
3386
|
+
async appendMany(entries) {
|
|
3387
|
+
await ensureMigrated();
|
|
3388
|
+
for (const entry of entries) {
|
|
3389
|
+
appendOne(entry);
|
|
3591
3390
|
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
const
|
|
3596
|
-
|
|
3391
|
+
},
|
|
3392
|
+
async list(options2 = {}) {
|
|
3393
|
+
await ensureMigrated();
|
|
3394
|
+
const { sql, params } = buildWhereClause(options2);
|
|
3395
|
+
const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
|
|
3396
|
+
const stmt = db.prepare(
|
|
3397
|
+
`SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
|
|
3597
3398
|
);
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
}
|
|
3601
|
-
if (tokenBalance === 0 && parentApiKey) {
|
|
3602
|
-
try {
|
|
3603
|
-
const balanceInfo = await this.balanceManager.getTokenBalance(
|
|
3604
|
-
parentApiKey.key,
|
|
3605
|
-
baseUrl
|
|
3606
|
-
);
|
|
3607
|
-
tokenBalance = balanceInfo.amount;
|
|
3608
|
-
tokenBalanceUnit = balanceInfo.unit;
|
|
3609
|
-
} catch (e) {
|
|
3610
|
-
this._log("WARN", "Could not get initial API key balance:", e);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
this._log(
|
|
3614
|
-
"DEBUG",
|
|
3615
|
-
`[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
|
|
3616
|
-
);
|
|
3617
|
-
return {
|
|
3618
|
-
token: parentApiKey?.key ?? "",
|
|
3619
|
-
tokenBalance,
|
|
3620
|
-
tokenBalanceUnit
|
|
3621
|
-
};
|
|
3622
|
-
}
|
|
3623
|
-
this._log(
|
|
3624
|
-
"DEBUG",
|
|
3625
|
-
`[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
|
|
3626
|
-
);
|
|
3627
|
-
const spendResult = await this.cashuSpender.spend({
|
|
3628
|
-
mintUrl,
|
|
3629
|
-
amount,
|
|
3630
|
-
baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
|
|
3631
|
-
reuseToken: this.mode === "lazyrefund"
|
|
3632
|
-
});
|
|
3633
|
-
if (!spendResult.token) {
|
|
3634
|
-
this._log(
|
|
3635
|
-
"ERROR",
|
|
3636
|
-
`[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
|
|
3637
|
-
spendResult.error
|
|
3638
|
-
);
|
|
3639
|
-
} else {
|
|
3640
|
-
this._log(
|
|
3641
|
-
"DEBUG",
|
|
3642
|
-
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
|
|
3399
|
+
const rows = stmt.all(
|
|
3400
|
+
...typeof options2.limit === "number" ? [...params, options2.limit] : params
|
|
3643
3401
|
);
|
|
3402
|
+
return rows.map(mapRow);
|
|
3403
|
+
},
|
|
3404
|
+
async count(options2 = {}) {
|
|
3405
|
+
await ensureMigrated();
|
|
3406
|
+
const { sql, params } = buildWhereClause(options2);
|
|
3407
|
+
const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
|
|
3408
|
+
const row = stmt.get(...params);
|
|
3409
|
+
return Number(row?.count ?? 0);
|
|
3410
|
+
},
|
|
3411
|
+
async deleteOlderThan(timestamp) {
|
|
3412
|
+
await ensureMigrated();
|
|
3413
|
+
const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
|
|
3414
|
+
const result = stmt.run(timestamp);
|
|
3415
|
+
return result.changes;
|
|
3416
|
+
},
|
|
3417
|
+
async clear() {
|
|
3418
|
+
await ensureMigrated();
|
|
3419
|
+
db.prepare(`DELETE FROM ${tableName}`).run();
|
|
3644
3420
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3421
|
+
};
|
|
3422
|
+
};
|
|
3423
|
+
|
|
3424
|
+
// storage/usageTracking/memory.ts
|
|
3425
|
+
var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3426
|
+
var matchesFilters2 = (entry, options = {}) => {
|
|
3427
|
+
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
3428
|
+
return false;
|
|
3650
3429
|
}
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
*/
|
|
3654
|
-
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
3655
|
-
const headers = {
|
|
3656
|
-
...additionalHeaders,
|
|
3657
|
-
"Content-Type": "application/json"
|
|
3658
|
-
};
|
|
3659
|
-
return headers;
|
|
3430
|
+
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
3431
|
+
return false;
|
|
3660
3432
|
}
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
*/
|
|
3664
|
-
_withAuthHeader(headers, token) {
|
|
3665
|
-
const nextHeaders = { ...headers };
|
|
3666
|
-
if (this.mode === "xcashu") {
|
|
3667
|
-
nextHeaders["X-Cashu"] = token;
|
|
3668
|
-
} else {
|
|
3669
|
-
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
3670
|
-
}
|
|
3671
|
-
return nextHeaders;
|
|
3433
|
+
if (options.modelId && entry.modelId !== options.modelId) {
|
|
3434
|
+
return false;
|
|
3672
3435
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
// storage/drivers/localStorage.ts
|
|
3676
|
-
var canUseLocalStorage = () => {
|
|
3677
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
3678
|
-
};
|
|
3679
|
-
var isQuotaExceeded = (error) => {
|
|
3680
|
-
const e = error;
|
|
3681
|
-
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
3682
|
-
};
|
|
3683
|
-
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
3684
|
-
var localStorageDriver = {
|
|
3685
|
-
async getItem(key, defaultValue) {
|
|
3686
|
-
if (!canUseLocalStorage()) return defaultValue;
|
|
3687
|
-
try {
|
|
3688
|
-
const item = window.localStorage.getItem(key);
|
|
3689
|
-
if (item === null) return defaultValue;
|
|
3690
|
-
try {
|
|
3691
|
-
return JSON.parse(item);
|
|
3692
|
-
} catch (parseError) {
|
|
3693
|
-
if (typeof defaultValue === "string") {
|
|
3694
|
-
return item;
|
|
3695
|
-
}
|
|
3696
|
-
throw parseError;
|
|
3697
|
-
}
|
|
3698
|
-
} catch (error) {
|
|
3699
|
-
console.error(`Error retrieving item with key "${key}":`, error);
|
|
3700
|
-
if (canUseLocalStorage()) {
|
|
3701
|
-
try {
|
|
3702
|
-
window.localStorage.removeItem(key);
|
|
3703
|
-
} catch (removeError) {
|
|
3704
|
-
console.error(
|
|
3705
|
-
`Error removing corrupted item with key "${key}":`,
|
|
3706
|
-
removeError
|
|
3707
|
-
);
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
return defaultValue;
|
|
3711
|
-
}
|
|
3712
|
-
},
|
|
3713
|
-
async setItem(key, value) {
|
|
3714
|
-
if (!canUseLocalStorage()) return;
|
|
3715
|
-
try {
|
|
3716
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
3717
|
-
} catch (error) {
|
|
3718
|
-
if (isQuotaExceeded(error)) {
|
|
3719
|
-
if (NON_CRITICAL_KEYS.has(key)) {
|
|
3720
|
-
console.warn(
|
|
3721
|
-
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
3722
|
-
);
|
|
3723
|
-
return;
|
|
3724
|
-
}
|
|
3725
|
-
try {
|
|
3726
|
-
window.localStorage.removeItem("modelsFromAllProviders");
|
|
3727
|
-
} catch {
|
|
3728
|
-
}
|
|
3729
|
-
try {
|
|
3730
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
3731
|
-
return;
|
|
3732
|
-
} catch (retryError) {
|
|
3733
|
-
console.warn(
|
|
3734
|
-
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
3735
|
-
retryError
|
|
3736
|
-
);
|
|
3737
|
-
return;
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
console.error(`Error storing item with key "${key}":`, error);
|
|
3741
|
-
}
|
|
3742
|
-
},
|
|
3743
|
-
async removeItem(key) {
|
|
3744
|
-
if (!canUseLocalStorage()) return;
|
|
3745
|
-
try {
|
|
3746
|
-
window.localStorage.removeItem(key);
|
|
3747
|
-
} catch (error) {
|
|
3748
|
-
console.error(`Error removing item with key "${key}":`, error);
|
|
3749
|
-
}
|
|
3436
|
+
if (options.baseUrl && normalizeBaseUrl3(entry.baseUrl) !== normalizeBaseUrl3(options.baseUrl)) {
|
|
3437
|
+
return false;
|
|
3750
3438
|
}
|
|
3439
|
+
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
3440
|
+
return false;
|
|
3441
|
+
}
|
|
3442
|
+
if (options.client && entry.client !== options.client) {
|
|
3443
|
+
return false;
|
|
3444
|
+
}
|
|
3445
|
+
return true;
|
|
3751
3446
|
};
|
|
3752
|
-
|
|
3753
|
-
// storage/drivers/memory.ts
|
|
3754
|
-
var createMemoryDriver = (seed) => {
|
|
3447
|
+
var createMemoryUsageTrackingDriver = (seed = []) => {
|
|
3755
3448
|
const store = /* @__PURE__ */ new Map();
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
store.set(key, value);
|
|
3759
|
-
}
|
|
3449
|
+
for (const entry of seed) {
|
|
3450
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3760
3451
|
}
|
|
3761
3452
|
return {
|
|
3762
|
-
async
|
|
3763
|
-
|
|
3764
|
-
if (item === void 0) return defaultValue;
|
|
3765
|
-
try {
|
|
3766
|
-
return JSON.parse(item);
|
|
3767
|
-
} catch (parseError) {
|
|
3768
|
-
if (typeof defaultValue === "string") {
|
|
3769
|
-
return item;
|
|
3770
|
-
}
|
|
3771
|
-
throw parseError;
|
|
3772
|
-
}
|
|
3453
|
+
async migrate() {
|
|
3454
|
+
return;
|
|
3773
3455
|
},
|
|
3774
|
-
async
|
|
3775
|
-
store.set(
|
|
3456
|
+
async append(entry) {
|
|
3457
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3776
3458
|
},
|
|
3777
|
-
async
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
};
|
|
3781
|
-
};
|
|
3782
|
-
|
|
3783
|
-
// storage/drivers/sqlite.ts
|
|
3784
|
-
var isBun = () => {
|
|
3785
|
-
return typeof process.versions.bun !== "undefined";
|
|
3786
|
-
};
|
|
3787
|
-
var createDatabase = (dbPath) => {
|
|
3788
|
-
if (isBun()) {
|
|
3789
|
-
throw new Error(
|
|
3790
|
-
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
3791
|
-
);
|
|
3792
|
-
}
|
|
3793
|
-
let Database = null;
|
|
3794
|
-
try {
|
|
3795
|
-
Database = __require("better-sqlite3");
|
|
3796
|
-
} catch (error) {
|
|
3797
|
-
throw new Error(
|
|
3798
|
-
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
3799
|
-
);
|
|
3800
|
-
}
|
|
3801
|
-
return new Database(dbPath);
|
|
3802
|
-
};
|
|
3803
|
-
var createSqliteDriver = (options = {}) => {
|
|
3804
|
-
const dbPath = options.dbPath || "routstr.sqlite";
|
|
3805
|
-
const tableName = options.tableName || "sdk_storage";
|
|
3806
|
-
const db = createDatabase(dbPath);
|
|
3807
|
-
db.exec(
|
|
3808
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
3809
|
-
);
|
|
3810
|
-
const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
3811
|
-
const upsertStmt = db.prepare(
|
|
3812
|
-
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
3813
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
3814
|
-
);
|
|
3815
|
-
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
3816
|
-
return {
|
|
3817
|
-
async getItem(key, defaultValue) {
|
|
3818
|
-
try {
|
|
3819
|
-
const row = selectStmt.get(key);
|
|
3820
|
-
if (!row || typeof row.value !== "string") return defaultValue;
|
|
3821
|
-
try {
|
|
3822
|
-
return JSON.parse(row.value);
|
|
3823
|
-
} catch (parseError) {
|
|
3824
|
-
if (typeof defaultValue === "string") {
|
|
3825
|
-
return row.value;
|
|
3826
|
-
}
|
|
3827
|
-
throw parseError;
|
|
3828
|
-
}
|
|
3829
|
-
} catch (error) {
|
|
3830
|
-
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
3831
|
-
return defaultValue;
|
|
3459
|
+
async appendMany(entries) {
|
|
3460
|
+
for (const entry of entries) {
|
|
3461
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3832
3462
|
}
|
|
3833
3463
|
},
|
|
3834
|
-
async
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
3464
|
+
async list(options = {}) {
|
|
3465
|
+
const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
|
|
3466
|
+
if (typeof options.limit === "number") {
|
|
3467
|
+
return entries.slice(0, options.limit);
|
|
3839
3468
|
}
|
|
3469
|
+
return entries;
|
|
3840
3470
|
},
|
|
3841
|
-
async
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3471
|
+
async count(options = {}) {
|
|
3472
|
+
return (await this.list(options)).length;
|
|
3473
|
+
},
|
|
3474
|
+
async deleteOlderThan(timestamp) {
|
|
3475
|
+
let deleted = 0;
|
|
3476
|
+
for (const [id, entry] of store.entries()) {
|
|
3477
|
+
if (entry.timestamp < timestamp) {
|
|
3478
|
+
store.delete(id);
|
|
3479
|
+
deleted += 1;
|
|
3480
|
+
}
|
|
3846
3481
|
}
|
|
3482
|
+
return deleted;
|
|
3483
|
+
},
|
|
3484
|
+
async clear() {
|
|
3485
|
+
store.clear();
|
|
3847
3486
|
}
|
|
3848
3487
|
};
|
|
3849
3488
|
};
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
db.createObjectStore(storeName);
|
|
3863
|
-
}
|
|
3864
|
-
};
|
|
3865
|
-
request.onsuccess = () => resolve(request.result);
|
|
3866
|
-
request.onerror = () => reject(request.error);
|
|
3867
|
-
});
|
|
3868
|
-
};
|
|
3869
|
-
var createIndexedDBDriver = (options = {}) => {
|
|
3870
|
-
const dbName = options.dbName || "routstr-sdk";
|
|
3871
|
-
const storeName = options.storeName || "sdk_storage";
|
|
3872
|
-
let dbPromise = null;
|
|
3873
|
-
const getDb = () => {
|
|
3874
|
-
if (!dbPromise) {
|
|
3875
|
-
dbPromise = openDatabase(dbName, storeName);
|
|
3876
|
-
}
|
|
3877
|
-
return dbPromise;
|
|
3878
|
-
};
|
|
3879
|
-
return {
|
|
3880
|
-
async getItem(key, defaultValue) {
|
|
3881
|
-
try {
|
|
3882
|
-
const db = await getDb();
|
|
3883
|
-
return new Promise((resolve, reject) => {
|
|
3884
|
-
const tx = db.transaction(storeName, "readonly");
|
|
3885
|
-
const store = tx.objectStore(storeName);
|
|
3886
|
-
const request = store.get(key);
|
|
3887
|
-
request.onsuccess = () => {
|
|
3888
|
-
const raw = request.result;
|
|
3889
|
-
if (raw === void 0) {
|
|
3890
|
-
resolve(defaultValue);
|
|
3891
|
-
return;
|
|
3892
|
-
}
|
|
3893
|
-
if (typeof raw === "string") {
|
|
3894
|
-
try {
|
|
3895
|
-
resolve(JSON.parse(raw));
|
|
3896
|
-
} catch {
|
|
3897
|
-
if (typeof defaultValue === "string") {
|
|
3898
|
-
resolve(raw);
|
|
3899
|
-
} else {
|
|
3900
|
-
resolve(defaultValue);
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
} else {
|
|
3904
|
-
resolve(raw);
|
|
3905
|
-
}
|
|
3906
|
-
};
|
|
3907
|
-
request.onerror = () => reject(request.error);
|
|
3908
|
-
});
|
|
3909
|
-
} catch (error) {
|
|
3910
|
-
console.error(`IndexedDB getItem failed for key "${key}":`, error);
|
|
3911
|
-
return defaultValue;
|
|
3912
|
-
}
|
|
3913
|
-
},
|
|
3914
|
-
async setItem(key, value) {
|
|
3915
|
-
try {
|
|
3916
|
-
const db = await getDb();
|
|
3917
|
-
return new Promise((resolve, reject) => {
|
|
3918
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
3919
|
-
const store = tx.objectStore(storeName);
|
|
3920
|
-
store.put(JSON.stringify(value), key);
|
|
3921
|
-
tx.oncomplete = () => resolve();
|
|
3922
|
-
tx.onerror = () => reject(tx.error);
|
|
3923
|
-
});
|
|
3924
|
-
} catch (error) {
|
|
3925
|
-
console.error(`IndexedDB setItem failed for key "${key}":`, error);
|
|
3926
|
-
}
|
|
3927
|
-
},
|
|
3928
|
-
async removeItem(key) {
|
|
3929
|
-
try {
|
|
3930
|
-
const db = await getDb();
|
|
3931
|
-
return new Promise((resolve, reject) => {
|
|
3932
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
3933
|
-
const store = tx.objectStore(storeName);
|
|
3934
|
-
store.delete(key);
|
|
3935
|
-
tx.oncomplete = () => resolve();
|
|
3936
|
-
tx.onerror = () => reject(tx.error);
|
|
3937
|
-
});
|
|
3938
|
-
} catch (error) {
|
|
3939
|
-
console.error(`IndexedDB removeItem failed for key "${key}":`, error);
|
|
3940
|
-
}
|
|
3941
|
-
}
|
|
3942
|
-
};
|
|
3943
|
-
};
|
|
3944
|
-
|
|
3945
|
-
// storage/keys.ts
|
|
3946
|
-
var SDK_STORAGE_KEYS = {
|
|
3947
|
-
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
3948
|
-
LAST_USED_MODEL: "lastUsedModel",
|
|
3949
|
-
BASE_URLS_LIST: "base_urls_list",
|
|
3950
|
-
DISABLED_PROVIDERS: "disabled_providers",
|
|
3951
|
-
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
3952
|
-
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
3953
|
-
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
3954
|
-
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
3955
|
-
LOCAL_CASHU_TOKENS: "local_cashu_tokens",
|
|
3956
|
-
API_KEYS: "api_keys",
|
|
3957
|
-
CHILD_KEYS: "child_keys",
|
|
3958
|
-
ROUTSTR21_MODELS: "routstr21Models",
|
|
3959
|
-
LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
|
|
3960
|
-
CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
|
|
3961
|
-
USAGE_TRACKING: "usage_tracking",
|
|
3962
|
-
CLIENT_IDS: "client_ids"
|
|
3963
|
-
};
|
|
3964
|
-
|
|
3965
|
-
// storage/store.ts
|
|
3966
|
-
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3967
|
-
var getCashuTokenBalance = (token) => {
|
|
3968
|
-
try {
|
|
3969
|
-
const decoded = cashuTs.getDecodedToken(token);
|
|
3970
|
-
const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
|
|
3971
|
-
let sum = 0;
|
|
3972
|
-
for (const proof of decoded.proofs) {
|
|
3973
|
-
sum += proof.amount / unitDivisor;
|
|
3974
|
-
}
|
|
3975
|
-
return sum;
|
|
3976
|
-
} catch {
|
|
3977
|
-
return 0;
|
|
3489
|
+
var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3490
|
+
var getCashuTokenBalance = (token) => {
|
|
3491
|
+
try {
|
|
3492
|
+
const decoded = cashuTs.getDecodedToken(token);
|
|
3493
|
+
const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
|
|
3494
|
+
let sum = 0;
|
|
3495
|
+
for (const proof of decoded.proofs) {
|
|
3496
|
+
sum += proof.amount / unitDivisor;
|
|
3497
|
+
}
|
|
3498
|
+
return sum;
|
|
3499
|
+
} catch {
|
|
3500
|
+
return 0;
|
|
3978
3501
|
}
|
|
3979
3502
|
};
|
|
3980
3503
|
var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
@@ -3992,12 +3515,11 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
3992
3515
|
routstr21Models: [],
|
|
3993
3516
|
lastRoutstr21ModelsUpdate: null,
|
|
3994
3517
|
cachedReceiveTokens: [],
|
|
3995
|
-
usageTracking: [],
|
|
3996
3518
|
clientIds: [],
|
|
3997
3519
|
setModelsFromAllProviders: (value) => {
|
|
3998
3520
|
const normalized = {};
|
|
3999
3521
|
for (const [baseUrl, models] of Object.entries(value)) {
|
|
4000
|
-
normalized[
|
|
3522
|
+
normalized[normalizeBaseUrl4(baseUrl)] = models;
|
|
4001
3523
|
}
|
|
4002
3524
|
void driver.setItem(
|
|
4003
3525
|
SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
|
|
@@ -4010,7 +3532,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4010
3532
|
set({ lastUsedModel: value });
|
|
4011
3533
|
},
|
|
4012
3534
|
setBaseUrlsList: (value) => {
|
|
4013
|
-
const normalized = value.map((url) =>
|
|
3535
|
+
const normalized = value.map((url) => normalizeBaseUrl4(url));
|
|
4014
3536
|
void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
|
|
4015
3537
|
set({ baseUrlsList: normalized });
|
|
4016
3538
|
},
|
|
@@ -4019,14 +3541,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4019
3541
|
set({ lastBaseUrlsUpdate: value });
|
|
4020
3542
|
},
|
|
4021
3543
|
setDisabledProviders: (value) => {
|
|
4022
|
-
const normalized = value.map((url) =>
|
|
3544
|
+
const normalized = value.map((url) => normalizeBaseUrl4(url));
|
|
4023
3545
|
void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
|
|
4024
3546
|
set({ disabledProviders: normalized });
|
|
4025
3547
|
},
|
|
4026
3548
|
setMintsFromAllProviders: (value) => {
|
|
4027
3549
|
const normalized = {};
|
|
4028
3550
|
for (const [baseUrl, mints] of Object.entries(value)) {
|
|
4029
|
-
normalized[
|
|
3551
|
+
normalized[normalizeBaseUrl4(baseUrl)] = mints.map(
|
|
4030
3552
|
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
4031
3553
|
);
|
|
4032
3554
|
}
|
|
@@ -4039,7 +3561,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4039
3561
|
setInfoFromAllProviders: (value) => {
|
|
4040
3562
|
const normalized = {};
|
|
4041
3563
|
for (const [baseUrl, info] of Object.entries(value)) {
|
|
4042
|
-
normalized[
|
|
3564
|
+
normalized[normalizeBaseUrl4(baseUrl)] = info;
|
|
4043
3565
|
}
|
|
4044
3566
|
void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
|
|
4045
3567
|
set({ infoFromAllProviders: normalized });
|
|
@@ -4047,7 +3569,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4047
3569
|
setLastModelsUpdate: (value) => {
|
|
4048
3570
|
const normalized = {};
|
|
4049
3571
|
for (const [baseUrl, timestamp] of Object.entries(value)) {
|
|
4050
|
-
normalized[
|
|
3572
|
+
normalized[normalizeBaseUrl4(baseUrl)] = timestamp;
|
|
4051
3573
|
}
|
|
4052
3574
|
void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
|
|
4053
3575
|
set({ lastModelsUpdate: normalized });
|
|
@@ -4057,7 +3579,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4057
3579
|
const updates = typeof value === "function" ? value(state.cachedTokens) : value;
|
|
4058
3580
|
const normalized = updates.map((entry) => ({
|
|
4059
3581
|
...entry,
|
|
4060
|
-
baseUrl:
|
|
3582
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4061
3583
|
balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
|
|
4062
3584
|
lastUsed: entry.lastUsed ?? null
|
|
4063
3585
|
}));
|
|
@@ -4070,7 +3592,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4070
3592
|
const updates = typeof value === "function" ? value(state.apiKeys) : value;
|
|
4071
3593
|
const normalized = updates.map((entry) => ({
|
|
4072
3594
|
...entry,
|
|
4073
|
-
baseUrl:
|
|
3595
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4074
3596
|
balance: entry.balance ?? 0,
|
|
4075
3597
|
lastUsed: entry.lastUsed ?? null
|
|
4076
3598
|
}));
|
|
@@ -4082,7 +3604,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4082
3604
|
set((state) => {
|
|
4083
3605
|
const updates = typeof value === "function" ? value(state.childKeys) : value;
|
|
4084
3606
|
const normalized = updates.map((entry) => ({
|
|
4085
|
-
parentBaseUrl:
|
|
3607
|
+
parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
|
|
4086
3608
|
childKey: entry.childKey,
|
|
4087
3609
|
balance: entry.balance ?? 0,
|
|
4088
3610
|
balanceLimit: entry.balanceLimit,
|
|
@@ -4111,10 +3633,6 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
|
4111
3633
|
void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
|
|
4112
3634
|
set({ cachedReceiveTokens: normalized });
|
|
4113
3635
|
},
|
|
4114
|
-
setUsageTracking: (value) => {
|
|
4115
|
-
void driver.setItem(SDK_STORAGE_KEYS.USAGE_TRACKING, value);
|
|
4116
|
-
set({ usageTracking: value });
|
|
4117
|
-
},
|
|
4118
3636
|
setClientIds: (value) => {
|
|
4119
3637
|
set((state) => {
|
|
4120
3638
|
const updates = typeof value === "function" ? value(state.clientIds) : value;
|
|
@@ -4144,7 +3662,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4144
3662
|
rawRoutstr21Models,
|
|
4145
3663
|
rawLastRoutstr21ModelsUpdate,
|
|
4146
3664
|
rawCachedReceiveTokens,
|
|
4147
|
-
rawUsageTracking,
|
|
4148
3665
|
rawClientIds
|
|
4149
3666
|
] = await Promise.all([
|
|
4150
3667
|
driver.getItem(
|
|
@@ -4176,51 +3693,50 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4176
3693
|
null
|
|
4177
3694
|
),
|
|
4178
3695
|
driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
|
|
4179
|
-
driver.getItem(SDK_STORAGE_KEYS.USAGE_TRACKING, []),
|
|
4180
3696
|
driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
|
|
4181
3697
|
]);
|
|
4182
3698
|
const modelsFromAllProviders = Object.fromEntries(
|
|
4183
3699
|
Object.entries(rawModels).map(([baseUrl, models]) => [
|
|
4184
|
-
|
|
3700
|
+
normalizeBaseUrl4(baseUrl),
|
|
4185
3701
|
models
|
|
4186
3702
|
])
|
|
4187
3703
|
);
|
|
4188
|
-
const baseUrlsList = rawBaseUrls.map((url) =>
|
|
3704
|
+
const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl4(url));
|
|
4189
3705
|
const disabledProviders = rawDisabledProviders.map(
|
|
4190
|
-
(url) =>
|
|
3706
|
+
(url) => normalizeBaseUrl4(url)
|
|
4191
3707
|
);
|
|
4192
3708
|
const mintsFromAllProviders = Object.fromEntries(
|
|
4193
3709
|
Object.entries(rawMints).map(([baseUrl, mints]) => [
|
|
4194
|
-
|
|
3710
|
+
normalizeBaseUrl4(baseUrl),
|
|
4195
3711
|
mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
|
|
4196
3712
|
])
|
|
4197
3713
|
);
|
|
4198
3714
|
const infoFromAllProviders = Object.fromEntries(
|
|
4199
3715
|
Object.entries(rawInfo).map(([baseUrl, info]) => [
|
|
4200
|
-
|
|
3716
|
+
normalizeBaseUrl4(baseUrl),
|
|
4201
3717
|
info
|
|
4202
3718
|
])
|
|
4203
3719
|
);
|
|
4204
3720
|
const lastModelsUpdate = Object.fromEntries(
|
|
4205
3721
|
Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
|
|
4206
|
-
|
|
3722
|
+
normalizeBaseUrl4(baseUrl),
|
|
4207
3723
|
timestamp
|
|
4208
3724
|
])
|
|
4209
3725
|
);
|
|
4210
3726
|
const cachedTokens = rawCachedTokens.map((entry) => ({
|
|
4211
3727
|
...entry,
|
|
4212
|
-
baseUrl:
|
|
3728
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4213
3729
|
balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
|
|
4214
3730
|
lastUsed: entry.lastUsed ?? null
|
|
4215
3731
|
}));
|
|
4216
3732
|
const apiKeys = rawApiKeys.map((entry) => ({
|
|
4217
3733
|
...entry,
|
|
4218
|
-
baseUrl:
|
|
3734
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4219
3735
|
balance: entry.balance ?? 0,
|
|
4220
3736
|
lastUsed: entry.lastUsed ?? null
|
|
4221
3737
|
}));
|
|
4222
3738
|
const childKeys = rawChildKeys.map((entry) => ({
|
|
4223
|
-
parentBaseUrl:
|
|
3739
|
+
parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
|
|
4224
3740
|
childKey: entry.childKey,
|
|
4225
3741
|
balance: entry.balance ?? 0,
|
|
4226
3742
|
balanceLimit: entry.balanceLimit,
|
|
@@ -4235,7 +3751,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4235
3751
|
unit: entry.unit || "sat",
|
|
4236
3752
|
createdAt: entry.createdAt ?? Date.now()
|
|
4237
3753
|
}));
|
|
4238
|
-
const usageTracking = rawUsageTracking;
|
|
4239
3754
|
const clientIds = rawClientIds.map((entry) => ({
|
|
4240
3755
|
...entry,
|
|
4241
3756
|
createdAt: entry.createdAt ?? Date.now(),
|
|
@@ -4256,7 +3771,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4256
3771
|
routstr21Models,
|
|
4257
3772
|
lastRoutstr21ModelsUpdate,
|
|
4258
3773
|
cachedReceiveTokens,
|
|
4259
|
-
usageTracking,
|
|
4260
3774
|
clientIds
|
|
4261
3775
|
});
|
|
4262
3776
|
};
|
|
@@ -4277,12 +3791,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
|
|
|
4277
3791
|
getCachedProviderInfo: () => store.getState().infoFromAllProviders,
|
|
4278
3792
|
setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
|
|
4279
3793
|
getProviderLastUpdate: (baseUrl) => {
|
|
4280
|
-
const normalized =
|
|
3794
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4281
3795
|
const timestamps = store.getState().lastModelsUpdate;
|
|
4282
3796
|
return timestamps[normalized] || null;
|
|
4283
3797
|
},
|
|
4284
3798
|
setProviderLastUpdate: (baseUrl, timestamp) => {
|
|
4285
|
-
const normalized =
|
|
3799
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4286
3800
|
const timestamps = { ...store.getState().lastModelsUpdate };
|
|
4287
3801
|
timestamps[normalized] = timestamp;
|
|
4288
3802
|
store.getState().setLastModelsUpdate(timestamps);
|
|
@@ -4301,7 +3815,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
|
|
|
4301
3815
|
});
|
|
4302
3816
|
var createStorageAdapterFromStore = (store) => ({
|
|
4303
3817
|
getToken: (baseUrl) => {
|
|
4304
|
-
const normalized =
|
|
3818
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4305
3819
|
const entry = store.getState().cachedTokens.find((token) => token.baseUrl === normalized);
|
|
4306
3820
|
if (!entry) return null;
|
|
4307
3821
|
const next = store.getState().cachedTokens.map(
|
|
@@ -4311,7 +3825,7 @@ var createStorageAdapterFromStore = (store) => ({
|
|
|
4311
3825
|
return entry.token;
|
|
4312
3826
|
},
|
|
4313
3827
|
setToken: (baseUrl, token) => {
|
|
4314
|
-
const normalized =
|
|
3828
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4315
3829
|
const tokens = store.getState().cachedTokens;
|
|
4316
3830
|
const balance = getCashuTokenBalance(token);
|
|
4317
3831
|
const existingIndex = tokens.findIndex(
|
|
@@ -4320,266 +3834,1532 @@ var createStorageAdapterFromStore = (store) => ({
|
|
|
4320
3834
|
if (existingIndex !== -1) {
|
|
4321
3835
|
throw new Error(`Token already exists for baseUrl: ${normalized}`);
|
|
4322
3836
|
}
|
|
4323
|
-
const next = [...tokens];
|
|
4324
|
-
next.push({
|
|
4325
|
-
baseUrl: normalized,
|
|
3837
|
+
const next = [...tokens];
|
|
3838
|
+
next.push({
|
|
3839
|
+
baseUrl: normalized,
|
|
3840
|
+
token,
|
|
3841
|
+
balance,
|
|
3842
|
+
lastUsed: Date.now()
|
|
3843
|
+
});
|
|
3844
|
+
store.getState().setCachedTokens(next);
|
|
3845
|
+
},
|
|
3846
|
+
removeToken: (baseUrl) => {
|
|
3847
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3848
|
+
const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
|
|
3849
|
+
store.getState().setCachedTokens(next);
|
|
3850
|
+
},
|
|
3851
|
+
updateTokenBalance: (baseUrl, balance) => {
|
|
3852
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3853
|
+
const tokens = store.getState().cachedTokens;
|
|
3854
|
+
const next = tokens.map(
|
|
3855
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
3856
|
+
);
|
|
3857
|
+
store.getState().setCachedTokens(next);
|
|
3858
|
+
},
|
|
3859
|
+
getCachedTokenDistribution: () => {
|
|
3860
|
+
const cachedTokens = store.getState().cachedTokens;
|
|
3861
|
+
const distributionMap = {};
|
|
3862
|
+
for (const entry of cachedTokens) {
|
|
3863
|
+
const sum = entry.balance || 0;
|
|
3864
|
+
if (sum > 0) {
|
|
3865
|
+
distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
|
|
3869
|
+
},
|
|
3870
|
+
getApiKeyDistribution: () => {
|
|
3871
|
+
const apiKeys = store.getState().apiKeys;
|
|
3872
|
+
const distributionMap = {};
|
|
3873
|
+
for (const entry of apiKeys) {
|
|
3874
|
+
const sum = entry.balance || 0;
|
|
3875
|
+
if (sum > 0) {
|
|
3876
|
+
distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
|
|
3880
|
+
},
|
|
3881
|
+
saveProviderInfo: (baseUrl, info) => {
|
|
3882
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3883
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
3884
|
+
next[normalized] = info;
|
|
3885
|
+
store.getState().setInfoFromAllProviders(next);
|
|
3886
|
+
},
|
|
3887
|
+
getProviderInfo: (baseUrl) => {
|
|
3888
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3889
|
+
return store.getState().infoFromAllProviders[normalized] || null;
|
|
3890
|
+
},
|
|
3891
|
+
// ========== API Keys (for apikeys mode) ==========
|
|
3892
|
+
getApiKey: (baseUrl) => {
|
|
3893
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3894
|
+
const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
|
|
3895
|
+
if (!entry) return null;
|
|
3896
|
+
const next = store.getState().apiKeys.map(
|
|
3897
|
+
(key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
|
|
3898
|
+
);
|
|
3899
|
+
store.getState().setApiKeys(next);
|
|
3900
|
+
return entry;
|
|
3901
|
+
},
|
|
3902
|
+
setApiKey: (baseUrl, key) => {
|
|
3903
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3904
|
+
const keys = store.getState().apiKeys;
|
|
3905
|
+
const existingIndex = keys.findIndex(
|
|
3906
|
+
(entry) => entry.baseUrl === normalized
|
|
3907
|
+
);
|
|
3908
|
+
if (existingIndex !== -1) {
|
|
3909
|
+
throw new Error(`ApiKey already exists for baseUrl: ${normalized}`);
|
|
3910
|
+
}
|
|
3911
|
+
const next = [...keys];
|
|
3912
|
+
next.push({
|
|
3913
|
+
baseUrl: normalized,
|
|
3914
|
+
key,
|
|
3915
|
+
balance: 0,
|
|
3916
|
+
lastUsed: Date.now()
|
|
3917
|
+
});
|
|
3918
|
+
store.getState().setApiKeys(next);
|
|
3919
|
+
},
|
|
3920
|
+
updateApiKeyBalance: (baseUrl, balance) => {
|
|
3921
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3922
|
+
const keys = store.getState().apiKeys;
|
|
3923
|
+
const next = keys.map(
|
|
3924
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
3925
|
+
);
|
|
3926
|
+
store.getState().setApiKeys(next);
|
|
3927
|
+
},
|
|
3928
|
+
removeApiKey: (baseUrl) => {
|
|
3929
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3930
|
+
const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
|
|
3931
|
+
store.getState().setApiKeys(next);
|
|
3932
|
+
},
|
|
3933
|
+
getAllApiKeys: () => {
|
|
3934
|
+
return store.getState().apiKeys.map((entry) => ({
|
|
3935
|
+
baseUrl: entry.baseUrl,
|
|
3936
|
+
key: entry.key,
|
|
3937
|
+
balance: entry.balance,
|
|
3938
|
+
lastUsed: entry.lastUsed
|
|
3939
|
+
}));
|
|
3940
|
+
},
|
|
3941
|
+
// ========== Child Keys ==========
|
|
3942
|
+
getChildKey: (parentBaseUrl) => {
|
|
3943
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3944
|
+
const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
|
|
3945
|
+
if (!entry) return null;
|
|
3946
|
+
return {
|
|
3947
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
3948
|
+
childKey: entry.childKey,
|
|
3949
|
+
balance: entry.balance,
|
|
3950
|
+
balanceLimit: entry.balanceLimit,
|
|
3951
|
+
validityDate: entry.validityDate,
|
|
3952
|
+
createdAt: entry.createdAt
|
|
3953
|
+
};
|
|
3954
|
+
},
|
|
3955
|
+
setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
|
|
3956
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3957
|
+
const keys = store.getState().childKeys;
|
|
3958
|
+
const existingIndex = keys.findIndex(
|
|
3959
|
+
(entry) => entry.parentBaseUrl === normalized
|
|
3960
|
+
);
|
|
3961
|
+
if (existingIndex !== -1) {
|
|
3962
|
+
const next = keys.map(
|
|
3963
|
+
(entry) => entry.parentBaseUrl === normalized ? {
|
|
3964
|
+
...entry,
|
|
3965
|
+
childKey,
|
|
3966
|
+
balance: balance ?? 0,
|
|
3967
|
+
validityDate,
|
|
3968
|
+
balanceLimit,
|
|
3969
|
+
createdAt: Date.now()
|
|
3970
|
+
} : entry
|
|
3971
|
+
);
|
|
3972
|
+
store.getState().setChildKeys(next);
|
|
3973
|
+
} else {
|
|
3974
|
+
const next = [...keys];
|
|
3975
|
+
next.push({
|
|
3976
|
+
parentBaseUrl: normalized,
|
|
3977
|
+
childKey,
|
|
3978
|
+
balance: balance ?? 0,
|
|
3979
|
+
validityDate,
|
|
3980
|
+
balanceLimit,
|
|
3981
|
+
createdAt: Date.now()
|
|
3982
|
+
});
|
|
3983
|
+
store.getState().setChildKeys(next);
|
|
3984
|
+
}
|
|
3985
|
+
},
|
|
3986
|
+
updateChildKeyBalance: (parentBaseUrl, balance) => {
|
|
3987
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3988
|
+
const keys = store.getState().childKeys;
|
|
3989
|
+
const next = keys.map(
|
|
3990
|
+
(entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
|
|
3991
|
+
);
|
|
3992
|
+
store.getState().setChildKeys(next);
|
|
3993
|
+
},
|
|
3994
|
+
removeChildKey: (parentBaseUrl) => {
|
|
3995
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3996
|
+
const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
|
|
3997
|
+
store.getState().setChildKeys(next);
|
|
3998
|
+
},
|
|
3999
|
+
getAllChildKeys: () => {
|
|
4000
|
+
return store.getState().childKeys.map((entry) => ({
|
|
4001
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
4002
|
+
childKey: entry.childKey,
|
|
4003
|
+
balance: entry.balance,
|
|
4004
|
+
balanceLimit: entry.balanceLimit,
|
|
4005
|
+
validityDate: entry.validityDate,
|
|
4006
|
+
createdAt: entry.createdAt
|
|
4007
|
+
}));
|
|
4008
|
+
},
|
|
4009
|
+
getCachedReceiveTokens: () => {
|
|
4010
|
+
return store.getState().cachedReceiveTokens;
|
|
4011
|
+
},
|
|
4012
|
+
setCachedReceiveTokens: (tokens) => {
|
|
4013
|
+
store.getState().setCachedReceiveTokens(tokens);
|
|
4014
|
+
}
|
|
4015
|
+
});
|
|
4016
|
+
var createProviderRegistryFromStore = (store) => ({
|
|
4017
|
+
getModelsForProvider: (baseUrl) => {
|
|
4018
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4019
|
+
return store.getState().modelsFromAllProviders[normalized] || [];
|
|
4020
|
+
},
|
|
4021
|
+
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4022
|
+
getProviderMints: (baseUrl) => {
|
|
4023
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4024
|
+
return store.getState().mintsFromAllProviders[normalized] || [];
|
|
4025
|
+
},
|
|
4026
|
+
getProviderInfo: async (baseUrl) => {
|
|
4027
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4028
|
+
const cached = store.getState().infoFromAllProviders[normalized];
|
|
4029
|
+
if (cached) return cached;
|
|
4030
|
+
try {
|
|
4031
|
+
const response = await fetch(`${normalized}v1/info`);
|
|
4032
|
+
if (!response.ok) {
|
|
4033
|
+
throw new Error(`Failed ${response.status}`);
|
|
4034
|
+
}
|
|
4035
|
+
const info = await response.json();
|
|
4036
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
4037
|
+
next[normalized] = info;
|
|
4038
|
+
store.getState().setInfoFromAllProviders(next);
|
|
4039
|
+
return info;
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
console.warn(`Failed to fetch provider info from ${normalized}:`, error);
|
|
4042
|
+
return null;
|
|
4043
|
+
}
|
|
4044
|
+
},
|
|
4045
|
+
getAllProvidersModels: () => store.getState().modelsFromAllProviders
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
// storage/index.ts
|
|
4049
|
+
var isBrowser3 = () => {
|
|
4050
|
+
try {
|
|
4051
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
4052
|
+
} catch {
|
|
4053
|
+
return false;
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
var isNode = () => {
|
|
4057
|
+
try {
|
|
4058
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
4059
|
+
} catch {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
var defaultDriver = null;
|
|
4064
|
+
var isBun3 = () => {
|
|
4065
|
+
return typeof process.versions.bun !== "undefined";
|
|
4066
|
+
};
|
|
4067
|
+
var getDefaultSdkDriver = () => {
|
|
4068
|
+
if (defaultDriver) return defaultDriver;
|
|
4069
|
+
if (isBrowser3()) {
|
|
4070
|
+
defaultDriver = localStorageDriver;
|
|
4071
|
+
return defaultDriver;
|
|
4072
|
+
}
|
|
4073
|
+
if (isBun3()) {
|
|
4074
|
+
defaultDriver = createMemoryDriver();
|
|
4075
|
+
return defaultDriver;
|
|
4076
|
+
}
|
|
4077
|
+
if (isNode()) {
|
|
4078
|
+
defaultDriver = createSqliteDriver();
|
|
4079
|
+
return defaultDriver;
|
|
4080
|
+
}
|
|
4081
|
+
defaultDriver = createMemoryDriver();
|
|
4082
|
+
return defaultDriver;
|
|
4083
|
+
};
|
|
4084
|
+
var defaultStore = null;
|
|
4085
|
+
var defaultUsageTrackingDriver = null;
|
|
4086
|
+
var getDefaultSdkStore = () => {
|
|
4087
|
+
if (!defaultStore) {
|
|
4088
|
+
defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
|
|
4089
|
+
}
|
|
4090
|
+
return defaultStore.hydrate.then(() => defaultStore.store);
|
|
4091
|
+
};
|
|
4092
|
+
var getDefaultUsageTrackingDriver = () => {
|
|
4093
|
+
if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
|
|
4094
|
+
const storageDriver = getDefaultSdkDriver();
|
|
4095
|
+
if (isBrowser3()) {
|
|
4096
|
+
defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
|
|
4097
|
+
legacyStorageDriver: storageDriver
|
|
4098
|
+
});
|
|
4099
|
+
return defaultUsageTrackingDriver;
|
|
4100
|
+
}
|
|
4101
|
+
if (isBun3()) {
|
|
4102
|
+
defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
|
|
4103
|
+
return defaultUsageTrackingDriver;
|
|
4104
|
+
}
|
|
4105
|
+
if (isNode()) {
|
|
4106
|
+
defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
|
|
4107
|
+
legacyStorageDriver: storageDriver
|
|
4108
|
+
});
|
|
4109
|
+
return defaultUsageTrackingDriver;
|
|
4110
|
+
}
|
|
4111
|
+
defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
|
|
4112
|
+
return defaultUsageTrackingDriver;
|
|
4113
|
+
};
|
|
4114
|
+
var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
|
|
4115
|
+
var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
|
|
4116
|
+
var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
|
|
4117
|
+
function createSSEParserTransform(onUsage, onResponseId) {
|
|
4118
|
+
let buffer = "";
|
|
4119
|
+
const maybeCaptureUsageFromJson = (jsonText) => {
|
|
4120
|
+
try {
|
|
4121
|
+
const data = JSON.parse(jsonText);
|
|
4122
|
+
const responseId = data.id;
|
|
4123
|
+
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
4124
|
+
onResponseId?.(responseId.trim());
|
|
4125
|
+
}
|
|
4126
|
+
const usage = extractUsageFromSSEJson(data);
|
|
4127
|
+
if (usage) {
|
|
4128
|
+
onUsage(usage);
|
|
4129
|
+
}
|
|
4130
|
+
} catch {
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
const processLine = (self, line) => {
|
|
4134
|
+
const trimmed = line.trim();
|
|
4135
|
+
if (!trimmed) {
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4138
|
+
if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
|
|
4139
|
+
self.push("data: [DONE]\n\n");
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
if (trimmed.startsWith("data:")) {
|
|
4143
|
+
const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
|
|
4144
|
+
if (dataStr === "[DONE]") {
|
|
4145
|
+
self.push("data: [DONE]\n\n");
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
maybeCaptureUsageFromJson(dataStr);
|
|
4149
|
+
self.push(`data: ${dataStr}
|
|
4150
|
+
|
|
4151
|
+
`);
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
if (trimmed.startsWith("{")) {
|
|
4155
|
+
maybeCaptureUsageFromJson(trimmed);
|
|
4156
|
+
self.push(`data: ${trimmed}
|
|
4157
|
+
|
|
4158
|
+
`);
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
self.push(line + "\n");
|
|
4162
|
+
};
|
|
4163
|
+
return new stream.Transform({
|
|
4164
|
+
transform(chunk, encoding, callback) {
|
|
4165
|
+
buffer += chunk.toString();
|
|
4166
|
+
const lines = buffer.split(/\r?\n/);
|
|
4167
|
+
buffer = lines.pop() || "";
|
|
4168
|
+
for (const line of lines) {
|
|
4169
|
+
processLine(this, line);
|
|
4170
|
+
}
|
|
4171
|
+
callback();
|
|
4172
|
+
},
|
|
4173
|
+
flush(callback) {
|
|
4174
|
+
if (buffer.trim()) {
|
|
4175
|
+
processLine(this, buffer);
|
|
4176
|
+
}
|
|
4177
|
+
buffer = "";
|
|
4178
|
+
callback();
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
4182
|
+
var TOPUP_MARGIN = 1.2;
|
|
4183
|
+
var RoutstrClient = class {
|
|
4184
|
+
constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
|
|
4185
|
+
this.walletAdapter = walletAdapter;
|
|
4186
|
+
this.storageAdapter = storageAdapter;
|
|
4187
|
+
this.providerRegistry = providerRegistry;
|
|
4188
|
+
this.balanceManager = new BalanceManager(
|
|
4189
|
+
walletAdapter,
|
|
4190
|
+
storageAdapter,
|
|
4191
|
+
providerRegistry
|
|
4192
|
+
);
|
|
4193
|
+
this.cashuSpender = new CashuSpender(
|
|
4194
|
+
walletAdapter,
|
|
4195
|
+
storageAdapter,
|
|
4196
|
+
providerRegistry,
|
|
4197
|
+
this.balanceManager
|
|
4198
|
+
);
|
|
4199
|
+
this.streamProcessor = new StreamProcessor();
|
|
4200
|
+
this.providerManager = new ProviderManager(providerRegistry);
|
|
4201
|
+
this.alertLevel = alertLevel;
|
|
4202
|
+
if (mode === "lazyrefund") {
|
|
4203
|
+
this.mode = "apikeys";
|
|
4204
|
+
} else if (mode === "apikeys") {
|
|
4205
|
+
this.mode = "lazyrefund";
|
|
4206
|
+
} else {
|
|
4207
|
+
this.mode = mode;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
cashuSpender;
|
|
4211
|
+
balanceManager;
|
|
4212
|
+
streamProcessor;
|
|
4213
|
+
providerManager;
|
|
4214
|
+
alertLevel;
|
|
4215
|
+
mode;
|
|
4216
|
+
debugLevel = "WARN";
|
|
4217
|
+
/**
|
|
4218
|
+
* Get the current client mode
|
|
4219
|
+
*/
|
|
4220
|
+
getMode() {
|
|
4221
|
+
return this.mode;
|
|
4222
|
+
}
|
|
4223
|
+
getDebugLevel() {
|
|
4224
|
+
return this.debugLevel;
|
|
4225
|
+
}
|
|
4226
|
+
setDebugLevel(level) {
|
|
4227
|
+
this.debugLevel = level;
|
|
4228
|
+
}
|
|
4229
|
+
_log(level, ...args) {
|
|
4230
|
+
const levelPriority = {
|
|
4231
|
+
DEBUG: 0,
|
|
4232
|
+
WARN: 1,
|
|
4233
|
+
ERROR: 2
|
|
4234
|
+
};
|
|
4235
|
+
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
4236
|
+
switch (level) {
|
|
4237
|
+
case "DEBUG":
|
|
4238
|
+
console.log(...args);
|
|
4239
|
+
break;
|
|
4240
|
+
case "WARN":
|
|
4241
|
+
console.warn(...args);
|
|
4242
|
+
break;
|
|
4243
|
+
case "ERROR":
|
|
4244
|
+
console.error(...args);
|
|
4245
|
+
break;
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Get the CashuSpender instance
|
|
4251
|
+
*/
|
|
4252
|
+
getCashuSpender() {
|
|
4253
|
+
return this.cashuSpender;
|
|
4254
|
+
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Get the BalanceManager instance
|
|
4257
|
+
*/
|
|
4258
|
+
getBalanceManager() {
|
|
4259
|
+
return this.balanceManager;
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* Get the ProviderManager instance
|
|
4263
|
+
*/
|
|
4264
|
+
getProviderManager() {
|
|
4265
|
+
return this.providerManager;
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Check if the client is currently busy (in critical section)
|
|
4269
|
+
*/
|
|
4270
|
+
get isBusy() {
|
|
4271
|
+
return this.cashuSpender.isBusy;
|
|
4272
|
+
}
|
|
4273
|
+
/**
|
|
4274
|
+
* Route an API request to the upstream provider
|
|
4275
|
+
*
|
|
4276
|
+
* This is a simpler alternative to fetchAIResponse that just proxies
|
|
4277
|
+
* the request upstream without the streaming callback machinery.
|
|
4278
|
+
* Useful for daemon-style routing where you just need to forward
|
|
4279
|
+
* requests and get responses back.
|
|
4280
|
+
*/
|
|
4281
|
+
async routeRequest(params) {
|
|
4282
|
+
const prepared = await this._prepareRoutedRequest(params);
|
|
4283
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4284
|
+
token: prepared.tokenUsed,
|
|
4285
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4286
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4287
|
+
response: prepared.response,
|
|
4288
|
+
modelId: prepared.modelId,
|
|
4289
|
+
usage: prepared.capturedUsage,
|
|
4290
|
+
requestId: prepared.capturedResponseId
|
|
4291
|
+
});
|
|
4292
|
+
prepared.response.satsSpent = satsSpent;
|
|
4293
|
+
prepared.response.usage = prepared.capturedUsage;
|
|
4294
|
+
prepared.response.requestId = prepared.capturedResponseId;
|
|
4295
|
+
return prepared.response;
|
|
4296
|
+
}
|
|
4297
|
+
async routeRequestToNodeResponse(params) {
|
|
4298
|
+
const { res } = params;
|
|
4299
|
+
const prepared = await this._prepareRoutedRequest(params);
|
|
4300
|
+
res.statusCode = prepared.response.status;
|
|
4301
|
+
prepared.response.headers.forEach((value, key) => {
|
|
4302
|
+
res.setHeader(key, value);
|
|
4303
|
+
});
|
|
4304
|
+
const body = prepared.response.body;
|
|
4305
|
+
if (!body) {
|
|
4306
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4307
|
+
token: prepared.tokenUsed,
|
|
4308
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4309
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4310
|
+
response: prepared.response,
|
|
4311
|
+
modelId: prepared.modelId,
|
|
4312
|
+
usage: prepared.capturedUsage,
|
|
4313
|
+
requestId: prepared.capturedResponseId
|
|
4314
|
+
});
|
|
4315
|
+
prepared.response.satsSpent = satsSpent;
|
|
4316
|
+
res.end();
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
const nodeReadable = stream.Readable.fromWeb(body);
|
|
4320
|
+
await new Promise((resolve, reject) => {
|
|
4321
|
+
let settled = false;
|
|
4322
|
+
const finish = async () => {
|
|
4323
|
+
if (settled) return;
|
|
4324
|
+
settled = true;
|
|
4325
|
+
try {
|
|
4326
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4327
|
+
token: prepared.tokenUsed,
|
|
4328
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4329
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4330
|
+
response: prepared.response,
|
|
4331
|
+
modelId: prepared.modelId,
|
|
4332
|
+
usage: prepared.capturedUsage,
|
|
4333
|
+
requestId: prepared.capturedResponseId
|
|
4334
|
+
});
|
|
4335
|
+
prepared.response.satsSpent = satsSpent;
|
|
4336
|
+
prepared.response.usage = prepared.capturedUsage;
|
|
4337
|
+
prepared.response.requestId = prepared.capturedResponseId;
|
|
4338
|
+
resolve();
|
|
4339
|
+
} catch (error) {
|
|
4340
|
+
reject(error);
|
|
4341
|
+
}
|
|
4342
|
+
};
|
|
4343
|
+
const fail = (error) => {
|
|
4344
|
+
if (settled) return;
|
|
4345
|
+
settled = true;
|
|
4346
|
+
reject(error);
|
|
4347
|
+
};
|
|
4348
|
+
res.once("finish", finish);
|
|
4349
|
+
res.once("close", finish);
|
|
4350
|
+
res.once("error", fail);
|
|
4351
|
+
nodeReadable.once("error", fail);
|
|
4352
|
+
nodeReadable.pipe(res);
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
async _prepareRoutedRequest(params) {
|
|
4356
|
+
const {
|
|
4357
|
+
path,
|
|
4358
|
+
method,
|
|
4359
|
+
body,
|
|
4360
|
+
headers = {},
|
|
4361
|
+
baseUrl,
|
|
4362
|
+
mintUrl,
|
|
4363
|
+
modelId
|
|
4364
|
+
} = params;
|
|
4365
|
+
await this._checkBalance();
|
|
4366
|
+
let requiredSats = 1;
|
|
4367
|
+
let selectedModel;
|
|
4368
|
+
if (modelId) {
|
|
4369
|
+
const providerModel = await this.providerManager.getModelForProvider(
|
|
4370
|
+
baseUrl,
|
|
4371
|
+
modelId
|
|
4372
|
+
);
|
|
4373
|
+
selectedModel = providerModel ?? void 0;
|
|
4374
|
+
if (selectedModel) {
|
|
4375
|
+
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4376
|
+
selectedModel,
|
|
4377
|
+
[]
|
|
4378
|
+
);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
|
|
4382
|
+
mintUrl,
|
|
4383
|
+
amount: requiredSats,
|
|
4384
|
+
baseUrl
|
|
4385
|
+
});
|
|
4386
|
+
this._log("DEBUG", token, baseUrl);
|
|
4387
|
+
let requestBody = body;
|
|
4388
|
+
if (body && typeof body === "object") {
|
|
4389
|
+
const bodyObj = body;
|
|
4390
|
+
if (!bodyObj.stream) {
|
|
4391
|
+
requestBody = { ...bodyObj, stream: false };
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
4395
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
4396
|
+
const response = await this._makeRequest({
|
|
4397
|
+
path,
|
|
4398
|
+
method,
|
|
4399
|
+
body: method === "GET" ? void 0 : requestBody,
|
|
4400
|
+
baseUrl,
|
|
4401
|
+
mintUrl,
|
|
4402
|
+
token,
|
|
4403
|
+
requiredSats,
|
|
4404
|
+
headers: requestHeaders,
|
|
4405
|
+
baseHeaders,
|
|
4406
|
+
selectedModel
|
|
4407
|
+
});
|
|
4408
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
4409
|
+
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
4410
|
+
const tokenUsed = response.token || token;
|
|
4411
|
+
const contentType = response.headers.get("content-type") || "";
|
|
4412
|
+
let processedResponse = response;
|
|
4413
|
+
let capturedUsage;
|
|
4414
|
+
let capturedResponseId;
|
|
4415
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
4416
|
+
const nodeReadable = stream.Readable.fromWeb(response.body);
|
|
4417
|
+
const sseParser = createSSEParserTransform(
|
|
4418
|
+
(usage) => {
|
|
4419
|
+
capturedUsage = usage;
|
|
4420
|
+
processedResponse.usage = usage;
|
|
4421
|
+
},
|
|
4422
|
+
(responseId) => {
|
|
4423
|
+
capturedResponseId = responseId;
|
|
4424
|
+
processedResponse.requestId = responseId;
|
|
4425
|
+
}
|
|
4426
|
+
);
|
|
4427
|
+
const transformed = nodeReadable.pipe(sseParser, { end: true });
|
|
4428
|
+
const webStream = stream.Readable.toWeb(
|
|
4429
|
+
transformed
|
|
4430
|
+
);
|
|
4431
|
+
processedResponse = new Response(webStream, {
|
|
4432
|
+
status: response.status,
|
|
4433
|
+
statusText: response.statusText,
|
|
4434
|
+
headers: response.headers
|
|
4435
|
+
});
|
|
4436
|
+
processedResponse.baseUrl = response.baseUrl;
|
|
4437
|
+
processedResponse.token = response.token;
|
|
4438
|
+
}
|
|
4439
|
+
return {
|
|
4440
|
+
response: processedResponse,
|
|
4441
|
+
tokenUsed,
|
|
4442
|
+
baseUrlUsed,
|
|
4443
|
+
tokenBalanceInSats,
|
|
4444
|
+
modelId,
|
|
4445
|
+
capturedUsage,
|
|
4446
|
+
capturedResponseId
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Fetch AI response with streaming
|
|
4451
|
+
*/
|
|
4452
|
+
async fetchAIResponse(options, callbacks) {
|
|
4453
|
+
const {
|
|
4454
|
+
messageHistory,
|
|
4455
|
+
selectedModel,
|
|
4456
|
+
baseUrl,
|
|
4457
|
+
mintUrl,
|
|
4458
|
+
balance,
|
|
4459
|
+
transactionHistory,
|
|
4460
|
+
maxTokens,
|
|
4461
|
+
headers
|
|
4462
|
+
} = options;
|
|
4463
|
+
const apiMessages = await this._convertMessages(messageHistory);
|
|
4464
|
+
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4465
|
+
selectedModel,
|
|
4466
|
+
apiMessages,
|
|
4467
|
+
maxTokens
|
|
4468
|
+
);
|
|
4469
|
+
try {
|
|
4470
|
+
await this._checkBalance();
|
|
4471
|
+
callbacks.onPaymentProcessing?.(true);
|
|
4472
|
+
const spendResult = await this._spendToken({
|
|
4473
|
+
mintUrl,
|
|
4474
|
+
amount: requiredSats,
|
|
4475
|
+
baseUrl
|
|
4476
|
+
});
|
|
4477
|
+
let token = spendResult.token;
|
|
4478
|
+
let tokenBalance = spendResult.tokenBalance;
|
|
4479
|
+
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
4480
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
4481
|
+
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
4482
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
4483
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
4484
|
+
this.providerManager.resetFailedProviders();
|
|
4485
|
+
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
4486
|
+
const providerVersion = providerInfo?.version ?? "";
|
|
4487
|
+
let modelIdForRequest = selectedModel.id;
|
|
4488
|
+
if (/^0\.1\./.test(providerVersion)) {
|
|
4489
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
4490
|
+
baseUrl,
|
|
4491
|
+
selectedModel.id
|
|
4492
|
+
);
|
|
4493
|
+
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
4494
|
+
}
|
|
4495
|
+
const body = {
|
|
4496
|
+
model: modelIdForRequest,
|
|
4497
|
+
messages: apiMessages,
|
|
4498
|
+
stream: true
|
|
4499
|
+
};
|
|
4500
|
+
if (maxTokens !== void 0) {
|
|
4501
|
+
body.max_tokens = maxTokens;
|
|
4502
|
+
}
|
|
4503
|
+
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
4504
|
+
body.tools = [{ type: "web_search" }];
|
|
4505
|
+
}
|
|
4506
|
+
const response = await this._makeRequest({
|
|
4507
|
+
path: "/v1/chat/completions",
|
|
4508
|
+
method: "POST",
|
|
4509
|
+
body,
|
|
4510
|
+
selectedModel,
|
|
4511
|
+
baseUrl,
|
|
4512
|
+
mintUrl,
|
|
4513
|
+
token,
|
|
4514
|
+
requiredSats,
|
|
4515
|
+
maxTokens,
|
|
4516
|
+
headers: requestHeaders,
|
|
4517
|
+
baseHeaders
|
|
4518
|
+
});
|
|
4519
|
+
if (!response.body) {
|
|
4520
|
+
throw new Error("Response body is not available");
|
|
4521
|
+
}
|
|
4522
|
+
if (response.status === 200) {
|
|
4523
|
+
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
4524
|
+
const streamingResult = await this.streamProcessor.process(
|
|
4525
|
+
response,
|
|
4526
|
+
{
|
|
4527
|
+
onContent: callbacks.onStreamingUpdate,
|
|
4528
|
+
onThinking: callbacks.onThinkingUpdate
|
|
4529
|
+
},
|
|
4530
|
+
selectedModel.id
|
|
4531
|
+
);
|
|
4532
|
+
if (streamingResult.finish_reason === "content_filter") {
|
|
4533
|
+
callbacks.onMessageAppend({
|
|
4534
|
+
role: "assistant",
|
|
4535
|
+
content: "Your request was denied due to content filtering."
|
|
4536
|
+
});
|
|
4537
|
+
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
4538
|
+
const message = await this._createAssistantMessage(streamingResult);
|
|
4539
|
+
callbacks.onMessageAppend(message);
|
|
4540
|
+
} else {
|
|
4541
|
+
callbacks.onMessageAppend({
|
|
4542
|
+
role: "system",
|
|
4543
|
+
content: "The provider did not respond to this request."
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
callbacks.onStreamingUpdate("");
|
|
4547
|
+
callbacks.onThinkingUpdate("");
|
|
4548
|
+
const isApikeysEstimate = this.mode === "apikeys";
|
|
4549
|
+
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4550
|
+
token,
|
|
4551
|
+
baseUrl: baseUrlUsed,
|
|
4552
|
+
initialTokenBalance: tokenBalanceInSats,
|
|
4553
|
+
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
4554
|
+
response,
|
|
4555
|
+
modelId: selectedModel.id,
|
|
4556
|
+
usage: streamingResult.usage ? {
|
|
4557
|
+
promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
|
|
4558
|
+
completionTokens: Number(
|
|
4559
|
+
streamingResult.usage.completion_tokens ?? 0
|
|
4560
|
+
),
|
|
4561
|
+
totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
|
|
4562
|
+
cost: Number(streamingResult.usage.cost ?? 0),
|
|
4563
|
+
satsCost: Number(streamingResult.usage.sats_cost ?? 0)
|
|
4564
|
+
} : void 0,
|
|
4565
|
+
requestId: streamingResult.responseId
|
|
4566
|
+
});
|
|
4567
|
+
const estimatedCosts = this._getEstimatedCosts(
|
|
4568
|
+
selectedModel,
|
|
4569
|
+
streamingResult
|
|
4570
|
+
);
|
|
4571
|
+
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
4572
|
+
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
4573
|
+
} else {
|
|
4574
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
4575
|
+
}
|
|
4576
|
+
} catch (error) {
|
|
4577
|
+
this._handleError(error, callbacks);
|
|
4578
|
+
} finally {
|
|
4579
|
+
callbacks.onPaymentProcessing?.(false);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
/**
|
|
4583
|
+
* Make the API request with failover support
|
|
4584
|
+
*/
|
|
4585
|
+
async _makeRequest(params) {
|
|
4586
|
+
const { path, method, body, baseUrl, token, headers } = params;
|
|
4587
|
+
try {
|
|
4588
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
4589
|
+
if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
|
|
4590
|
+
this._log("DEBUG", "HEADERS,", headers);
|
|
4591
|
+
const response = await fetch(url, {
|
|
4592
|
+
method,
|
|
4593
|
+
headers,
|
|
4594
|
+
body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
|
|
4595
|
+
});
|
|
4596
|
+
if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
|
|
4597
|
+
response.baseUrl = baseUrl;
|
|
4598
|
+
response.token = token;
|
|
4599
|
+
if (!response.ok) {
|
|
4600
|
+
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
4601
|
+
let bodyText;
|
|
4602
|
+
try {
|
|
4603
|
+
bodyText = await response.text();
|
|
4604
|
+
} catch (e) {
|
|
4605
|
+
bodyText = void 0;
|
|
4606
|
+
}
|
|
4607
|
+
return await this._handleErrorResponse(
|
|
4608
|
+
params,
|
|
4609
|
+
token,
|
|
4610
|
+
response.status,
|
|
4611
|
+
requestId,
|
|
4612
|
+
this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
|
|
4613
|
+
bodyText,
|
|
4614
|
+
params.retryCount ?? 0
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
return response;
|
|
4618
|
+
} catch (error) {
|
|
4619
|
+
if (isNetworkErrorMessage(error?.message || "")) {
|
|
4620
|
+
return await this._handleErrorResponse(
|
|
4621
|
+
params,
|
|
4622
|
+
token,
|
|
4623
|
+
-1,
|
|
4624
|
+
// just for Network Error to skip all statuses
|
|
4625
|
+
void 0,
|
|
4626
|
+
void 0,
|
|
4627
|
+
void 0,
|
|
4628
|
+
params.retryCount ?? 0
|
|
4629
|
+
);
|
|
4630
|
+
}
|
|
4631
|
+
throw error;
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
/**
|
|
4635
|
+
* Handle error responses with failover
|
|
4636
|
+
*/
|
|
4637
|
+
async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
|
|
4638
|
+
const MAX_RETRIES_PER_PROVIDER = 2;
|
|
4639
|
+
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
4640
|
+
let tryNextProvider = false;
|
|
4641
|
+
this._log(
|
|
4642
|
+
"DEBUG",
|
|
4643
|
+
`[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
|
|
4644
|
+
);
|
|
4645
|
+
this._log(
|
|
4646
|
+
"DEBUG",
|
|
4647
|
+
`[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
|
|
4648
|
+
);
|
|
4649
|
+
if (params.token.startsWith("cashu")) {
|
|
4650
|
+
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
4651
|
+
params.token
|
|
4652
|
+
);
|
|
4653
|
+
if (tryReceiveTokenResult.success) {
|
|
4654
|
+
this._log(
|
|
4655
|
+
"DEBUG",
|
|
4656
|
+
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
4657
|
+
);
|
|
4658
|
+
tryNextProvider = true;
|
|
4659
|
+
if (this.mode === "lazyrefund")
|
|
4660
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
4661
|
+
} else {
|
|
4662
|
+
this._log(
|
|
4663
|
+
"DEBUG",
|
|
4664
|
+
`[RoutstrClient] _handleErrorResponse: Failed to receive token. `
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
if (this.mode === "xcashu") {
|
|
4669
|
+
if (xCashuRefundToken) {
|
|
4670
|
+
this._log(
|
|
4671
|
+
"DEBUG",
|
|
4672
|
+
`[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
|
|
4673
|
+
);
|
|
4674
|
+
try {
|
|
4675
|
+
const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
|
|
4676
|
+
if (receiveResult.success) {
|
|
4677
|
+
this._log(
|
|
4678
|
+
"DEBUG",
|
|
4679
|
+
`[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
|
|
4680
|
+
);
|
|
4681
|
+
tryNextProvider = true;
|
|
4682
|
+
} else
|
|
4683
|
+
throw new ProviderError(
|
|
4684
|
+
baseUrl,
|
|
4685
|
+
status,
|
|
4686
|
+
"xcashu refund failed",
|
|
4687
|
+
requestId
|
|
4688
|
+
);
|
|
4689
|
+
} catch (error) {
|
|
4690
|
+
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
4691
|
+
throw new ProviderError(
|
|
4692
|
+
baseUrl,
|
|
4693
|
+
status,
|
|
4694
|
+
"[xcashu] Failed to receive refund token",
|
|
4695
|
+
requestId
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
} else {
|
|
4699
|
+
if (!tryNextProvider)
|
|
4700
|
+
throw new ProviderError(
|
|
4701
|
+
baseUrl,
|
|
4702
|
+
status,
|
|
4703
|
+
"[xcashu] Failed to receive refund token",
|
|
4704
|
+
requestId
|
|
4705
|
+
);
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
|
|
4709
|
+
this.storageAdapter.getApiKey(baseUrl);
|
|
4710
|
+
let topupAmount = params.requiredSats;
|
|
4711
|
+
try {
|
|
4712
|
+
let currentBalance = 0;
|
|
4713
|
+
if (this.mode === "apikeys") {
|
|
4714
|
+
const currentBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4715
|
+
params.token,
|
|
4716
|
+
baseUrl
|
|
4717
|
+
);
|
|
4718
|
+
currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
|
|
4719
|
+
} else if (this.mode === "lazyrefund") {
|
|
4720
|
+
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
4721
|
+
const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
|
|
4722
|
+
currentBalance = tokenEntry?.amount ?? 0;
|
|
4723
|
+
}
|
|
4724
|
+
const shortfall = Math.max(0, params.requiredSats - currentBalance);
|
|
4725
|
+
topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
|
|
4726
|
+
} catch (e) {
|
|
4727
|
+
this._log(
|
|
4728
|
+
"WARN",
|
|
4729
|
+
"Could not get current token balance for topup calculation:",
|
|
4730
|
+
e
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
const topupResult = await this.balanceManager.topUp({
|
|
4734
|
+
mintUrl,
|
|
4735
|
+
baseUrl,
|
|
4736
|
+
amount: topupAmount * TOPUP_MARGIN,
|
|
4737
|
+
token: params.token
|
|
4738
|
+
});
|
|
4739
|
+
this._log(
|
|
4740
|
+
"DEBUG",
|
|
4741
|
+
`[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
|
|
4742
|
+
);
|
|
4743
|
+
if (!topupResult.success) {
|
|
4744
|
+
const message = topupResult.message || "";
|
|
4745
|
+
if (message.includes("Insufficient balance")) {
|
|
4746
|
+
const needMatch = message.match(/need (\d+)/);
|
|
4747
|
+
const haveMatch = message.match(/have (\d+)/);
|
|
4748
|
+
const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
|
|
4749
|
+
const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
|
|
4750
|
+
this._log(
|
|
4751
|
+
"DEBUG",
|
|
4752
|
+
`[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
|
|
4753
|
+
);
|
|
4754
|
+
throw new InsufficientBalanceError(
|
|
4755
|
+
required,
|
|
4756
|
+
available,
|
|
4757
|
+
0,
|
|
4758
|
+
"",
|
|
4759
|
+
message
|
|
4760
|
+
);
|
|
4761
|
+
} else {
|
|
4762
|
+
this._log(
|
|
4763
|
+
"DEBUG",
|
|
4764
|
+
`[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
|
|
4765
|
+
);
|
|
4766
|
+
tryNextProvider = true;
|
|
4767
|
+
}
|
|
4768
|
+
} else {
|
|
4769
|
+
this._log(
|
|
4770
|
+
"DEBUG",
|
|
4771
|
+
`[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
if (!tryNextProvider) {
|
|
4775
|
+
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4776
|
+
this._log(
|
|
4777
|
+
"DEBUG",
|
|
4778
|
+
`[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4779
|
+
);
|
|
4780
|
+
return this._makeRequest({
|
|
4781
|
+
...params,
|
|
4782
|
+
token: params.token,
|
|
4783
|
+
headers: this._withAuthHeader(params.baseHeaders, params.token),
|
|
4784
|
+
retryCount: retryCount + 1
|
|
4785
|
+
});
|
|
4786
|
+
} else {
|
|
4787
|
+
this._log(
|
|
4788
|
+
"DEBUG",
|
|
4789
|
+
`[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4790
|
+
);
|
|
4791
|
+
tryNextProvider = true;
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
|
|
4796
|
+
if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
|
|
4797
|
+
let retryToken = params.token;
|
|
4798
|
+
try {
|
|
4799
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4800
|
+
params.token,
|
|
4801
|
+
baseUrl
|
|
4802
|
+
);
|
|
4803
|
+
if (latestBalanceInfo.isInvalidApiKey) {
|
|
4804
|
+
this._log(
|
|
4805
|
+
"DEBUG",
|
|
4806
|
+
`[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
|
|
4807
|
+
);
|
|
4808
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
4809
|
+
tryNextProvider = true;
|
|
4810
|
+
} else {
|
|
4811
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
4812
|
+
if (latestBalanceInfo.apiKey) {
|
|
4813
|
+
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
4814
|
+
if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
|
|
4815
|
+
if (storedApiKeyEntry) {
|
|
4816
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
4817
|
+
}
|
|
4818
|
+
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
4819
|
+
}
|
|
4820
|
+
retryToken = latestBalanceInfo.apiKey;
|
|
4821
|
+
}
|
|
4822
|
+
if (latestTokenBalance >= 0) {
|
|
4823
|
+
this.storageAdapter.updateApiKeyBalance(
|
|
4824
|
+
baseUrl,
|
|
4825
|
+
latestTokenBalance
|
|
4826
|
+
);
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
} catch (error) {
|
|
4830
|
+
this._log(
|
|
4831
|
+
"WARN",
|
|
4832
|
+
`[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
|
|
4833
|
+
error
|
|
4834
|
+
);
|
|
4835
|
+
}
|
|
4836
|
+
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4837
|
+
this._log(
|
|
4838
|
+
"DEBUG",
|
|
4839
|
+
`[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4840
|
+
);
|
|
4841
|
+
return this._makeRequest({
|
|
4842
|
+
...params,
|
|
4843
|
+
token: retryToken,
|
|
4844
|
+
headers: this._withAuthHeader(params.baseHeaders, retryToken),
|
|
4845
|
+
retryCount: retryCount + 1
|
|
4846
|
+
});
|
|
4847
|
+
} else {
|
|
4848
|
+
this._log(
|
|
4849
|
+
"DEBUG",
|
|
4850
|
+
`[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4851
|
+
);
|
|
4852
|
+
tryNextProvider = true;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
|
|
4856
|
+
this._log(
|
|
4857
|
+
"DEBUG",
|
|
4858
|
+
`[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
|
|
4859
|
+
);
|
|
4860
|
+
if (this.mode === "lazyrefund") {
|
|
4861
|
+
try {
|
|
4862
|
+
const refundResult = await this.balanceManager.refund({
|
|
4863
|
+
mintUrl,
|
|
4864
|
+
baseUrl,
|
|
4865
|
+
token: params.token
|
|
4866
|
+
});
|
|
4867
|
+
this._log(
|
|
4868
|
+
"DEBUG",
|
|
4869
|
+
`[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
|
|
4870
|
+
);
|
|
4871
|
+
if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
|
|
4872
|
+
else
|
|
4873
|
+
throw new ProviderError(
|
|
4874
|
+
baseUrl,
|
|
4875
|
+
status,
|
|
4876
|
+
"refund failed",
|
|
4877
|
+
requestId
|
|
4878
|
+
);
|
|
4879
|
+
} catch (error) {
|
|
4880
|
+
throw new ProviderError(
|
|
4881
|
+
baseUrl,
|
|
4882
|
+
status,
|
|
4883
|
+
"Failed to refund token",
|
|
4884
|
+
requestId
|
|
4885
|
+
);
|
|
4886
|
+
}
|
|
4887
|
+
} else if (this.mode === "apikeys") {
|
|
4888
|
+
this._log(
|
|
4889
|
+
"DEBUG",
|
|
4890
|
+
`[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
|
|
4891
|
+
);
|
|
4892
|
+
const initialBalance = await this.balanceManager.getTokenBalance(
|
|
4893
|
+
token,
|
|
4894
|
+
baseUrl
|
|
4895
|
+
);
|
|
4896
|
+
this._log(
|
|
4897
|
+
"DEBUG",
|
|
4898
|
+
`[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
|
|
4899
|
+
);
|
|
4900
|
+
const refundResult = await this.balanceManager.refundApiKey({
|
|
4901
|
+
mintUrl,
|
|
4902
|
+
baseUrl,
|
|
4903
|
+
apiKey: token
|
|
4904
|
+
});
|
|
4905
|
+
this._log(
|
|
4906
|
+
"DEBUG",
|
|
4907
|
+
`[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
|
|
4908
|
+
);
|
|
4909
|
+
if (!refundResult.success && initialBalance.amount > 0) {
|
|
4910
|
+
throw new ProviderError(
|
|
4911
|
+
baseUrl,
|
|
4912
|
+
status,
|
|
4913
|
+
refundResult.message ?? "Unknown error"
|
|
4914
|
+
);
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
this.providerManager.markFailed(baseUrl);
|
|
4919
|
+
this._log(
|
|
4920
|
+
"DEBUG",
|
|
4921
|
+
`[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
|
|
4922
|
+
);
|
|
4923
|
+
if (!selectedModel) {
|
|
4924
|
+
throw new ProviderError(
|
|
4925
|
+
baseUrl,
|
|
4926
|
+
status,
|
|
4927
|
+
"Funny, no selected model. HMM. "
|
|
4928
|
+
);
|
|
4929
|
+
}
|
|
4930
|
+
const nextProvider = this.providerManager.findNextBestProvider(
|
|
4931
|
+
selectedModel.id,
|
|
4932
|
+
baseUrl
|
|
4933
|
+
);
|
|
4934
|
+
if (nextProvider) {
|
|
4935
|
+
this._log(
|
|
4936
|
+
"DEBUG",
|
|
4937
|
+
`[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
|
|
4938
|
+
);
|
|
4939
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
4940
|
+
nextProvider,
|
|
4941
|
+
selectedModel.id
|
|
4942
|
+
) ?? selectedModel;
|
|
4943
|
+
const messagesForPricing = Array.isArray(
|
|
4944
|
+
body?.messages
|
|
4945
|
+
) ? body.messages : [];
|
|
4946
|
+
const newRequiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4947
|
+
newModel,
|
|
4948
|
+
messagesForPricing,
|
|
4949
|
+
params.maxTokens
|
|
4950
|
+
);
|
|
4951
|
+
this._log(
|
|
4952
|
+
"DEBUG",
|
|
4953
|
+
`[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
|
|
4954
|
+
);
|
|
4955
|
+
const spendResult = await this._spendToken({
|
|
4956
|
+
mintUrl,
|
|
4957
|
+
amount: newRequiredSats,
|
|
4958
|
+
baseUrl: nextProvider
|
|
4959
|
+
});
|
|
4960
|
+
return this._makeRequest({
|
|
4961
|
+
...params,
|
|
4962
|
+
path,
|
|
4963
|
+
method,
|
|
4964
|
+
body,
|
|
4965
|
+
baseUrl: nextProvider,
|
|
4966
|
+
selectedModel: newModel,
|
|
4967
|
+
token: spendResult.token,
|
|
4968
|
+
requiredSats: newRequiredSats,
|
|
4969
|
+
headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
|
|
4970
|
+
retryCount: 0
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
throw new FailoverError(
|
|
4974
|
+
baseUrl,
|
|
4975
|
+
Array.from(this.providerManager.getFailedProviders())
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* Handle post-response balance update for all modes
|
|
4980
|
+
*/
|
|
4981
|
+
async _handlePostResponseBalanceUpdate(params) {
|
|
4982
|
+
const {
|
|
4983
|
+
token,
|
|
4984
|
+
baseUrl,
|
|
4985
|
+
initialTokenBalance,
|
|
4986
|
+
fallbackSatsSpent,
|
|
4987
|
+
response,
|
|
4988
|
+
modelId,
|
|
4989
|
+
usage,
|
|
4990
|
+
requestId
|
|
4991
|
+
} = params;
|
|
4992
|
+
let satsSpent = initialTokenBalance;
|
|
4993
|
+
if (this.mode === "xcashu" && response) {
|
|
4994
|
+
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
4995
|
+
if (refundToken) {
|
|
4996
|
+
try {
|
|
4997
|
+
const receiveResult = await this.cashuSpender.receiveToken(refundToken);
|
|
4998
|
+
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
4999
|
+
} catch (error) {
|
|
5000
|
+
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
} else if (this.mode === "lazyrefund") {
|
|
5004
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
5005
|
+
token,
|
|
5006
|
+
baseUrl
|
|
5007
|
+
);
|
|
5008
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
5009
|
+
this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
|
|
5010
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
5011
|
+
} else if (this.mode === "apikeys") {
|
|
5012
|
+
try {
|
|
5013
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
5014
|
+
token,
|
|
5015
|
+
baseUrl
|
|
5016
|
+
);
|
|
5017
|
+
this._log(
|
|
5018
|
+
"DEBUG",
|
|
5019
|
+
"LATEST Balance",
|
|
5020
|
+
latestBalanceInfo.amount,
|
|
5021
|
+
latestBalanceInfo.reserved,
|
|
5022
|
+
latestBalanceInfo.apiKey,
|
|
5023
|
+
baseUrl
|
|
5024
|
+
);
|
|
5025
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
5026
|
+
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
5027
|
+
if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
|
|
5028
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
5029
|
+
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
5030
|
+
}
|
|
5031
|
+
this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
|
|
5032
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
5033
|
+
} catch (e) {
|
|
5034
|
+
this._log("WARN", "Could not get updated API key balance:", e);
|
|
5035
|
+
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
await this._trackResponseUsage({
|
|
4326
5039
|
token,
|
|
4327
|
-
|
|
4328
|
-
|
|
5040
|
+
baseUrl,
|
|
5041
|
+
response,
|
|
5042
|
+
modelId,
|
|
5043
|
+
satsSpent,
|
|
5044
|
+
usage,
|
|
5045
|
+
requestId
|
|
4329
5046
|
});
|
|
4330
|
-
|
|
4331
|
-
}
|
|
4332
|
-
|
|
4333
|
-
const
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
5047
|
+
return satsSpent;
|
|
5048
|
+
}
|
|
5049
|
+
async _trackResponseUsage(params) {
|
|
5050
|
+
const {
|
|
5051
|
+
token,
|
|
5052
|
+
baseUrl,
|
|
5053
|
+
response,
|
|
5054
|
+
modelId,
|
|
5055
|
+
satsSpent,
|
|
5056
|
+
usage: providedUsage,
|
|
5057
|
+
requestId: providedRequestId
|
|
5058
|
+
} = params;
|
|
5059
|
+
if (!response || !modelId) {
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
try {
|
|
5063
|
+
let usage = providedUsage;
|
|
5064
|
+
let requestId = providedRequestId;
|
|
5065
|
+
if (!usage || !requestId) {
|
|
5066
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5067
|
+
if (contentType.includes("text/event-stream")) {
|
|
5068
|
+
usage = usage ?? response.usage;
|
|
5069
|
+
requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
5070
|
+
if (!usage) {
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
} else {
|
|
5074
|
+
const cloned = response.clone();
|
|
5075
|
+
const responseBody = await cloned.json();
|
|
5076
|
+
usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
|
|
5077
|
+
requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
if (!usage) {
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
const finalRequestId = requestId || "unknown";
|
|
5084
|
+
const store = await getDefaultSdkStore();
|
|
5085
|
+
const state = store.getState();
|
|
5086
|
+
const matchingClient = state.clientIds.find(
|
|
5087
|
+
(client) => client.apiKey === token
|
|
5088
|
+
);
|
|
5089
|
+
const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
|
|
5090
|
+
const usageTracking = getDefaultUsageTrackingDriver();
|
|
5091
|
+
const entry = {
|
|
5092
|
+
id: entryId,
|
|
5093
|
+
timestamp: Date.now(),
|
|
5094
|
+
modelId,
|
|
5095
|
+
baseUrl,
|
|
5096
|
+
requestId: finalRequestId,
|
|
5097
|
+
client: matchingClient?.clientId,
|
|
5098
|
+
...usage
|
|
5099
|
+
};
|
|
5100
|
+
if (this.mode === "xcashu") {
|
|
5101
|
+
entry.satsCost = satsSpent;
|
|
5102
|
+
}
|
|
5103
|
+
await usageTracking.append(entry);
|
|
5104
|
+
} catch (error) {
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Convert messages for API format
|
|
5109
|
+
*/
|
|
5110
|
+
async _convertMessages(messages) {
|
|
5111
|
+
return Promise.all(
|
|
5112
|
+
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
5113
|
+
role: m.role,
|
|
5114
|
+
content: typeof m.content === "string" ? m.content : m.content
|
|
5115
|
+
}))
|
|
4342
5116
|
);
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
const
|
|
4350
|
-
if (
|
|
4351
|
-
|
|
5117
|
+
}
|
|
5118
|
+
/**
|
|
5119
|
+
* Create assistant message from streaming result
|
|
5120
|
+
*/
|
|
5121
|
+
async _createAssistantMessage(result) {
|
|
5122
|
+
if (result.images && result.images.length > 0) {
|
|
5123
|
+
const content = [];
|
|
5124
|
+
if (result.content) {
|
|
5125
|
+
content.push({
|
|
5126
|
+
type: "text",
|
|
5127
|
+
text: result.content,
|
|
5128
|
+
thinking: result.thinking,
|
|
5129
|
+
citations: result.citations,
|
|
5130
|
+
annotations: result.annotations
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
for (const img of result.images) {
|
|
5134
|
+
content.push({
|
|
5135
|
+
type: "image_url",
|
|
5136
|
+
image_url: {
|
|
5137
|
+
url: img.image_url.url
|
|
5138
|
+
}
|
|
5139
|
+
});
|
|
4352
5140
|
}
|
|
5141
|
+
return {
|
|
5142
|
+
role: "assistant",
|
|
5143
|
+
content
|
|
5144
|
+
};
|
|
4353
5145
|
}
|
|
4354
|
-
return
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
5146
|
+
return {
|
|
5147
|
+
role: "assistant",
|
|
5148
|
+
content: result.content || ""
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
/**
|
|
5152
|
+
* Calculate estimated costs from usage
|
|
5153
|
+
*/
|
|
5154
|
+
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
5155
|
+
let estimatedCosts = 0;
|
|
5156
|
+
if (streamingResult.usage) {
|
|
5157
|
+
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
5158
|
+
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
5159
|
+
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
4363
5160
|
}
|
|
4364
5161
|
}
|
|
4365
|
-
return
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
5162
|
+
return estimatedCosts;
|
|
5163
|
+
}
|
|
5164
|
+
/**
|
|
5165
|
+
* Get pending cashu token amount
|
|
5166
|
+
*/
|
|
5167
|
+
_getPendingCashuTokenAmount() {
|
|
5168
|
+
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
5169
|
+
return distribution.reduce((total, item) => total + item.amount, 0);
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Handle errors and notify callbacks
|
|
5173
|
+
*/
|
|
5174
|
+
_handleError(error, callbacks) {
|
|
5175
|
+
this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
|
|
5176
|
+
if (error instanceof Error) {
|
|
5177
|
+
const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
|
|
5178
|
+
const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
|
|
5179
|
+
this._log(
|
|
5180
|
+
"ERROR",
|
|
5181
|
+
`[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
|
|
5182
|
+
);
|
|
5183
|
+
callbacks.onMessageAppend({
|
|
5184
|
+
role: "system",
|
|
5185
|
+
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
5186
|
+
});
|
|
5187
|
+
} else {
|
|
5188
|
+
callbacks.onMessageAppend({
|
|
5189
|
+
role: "system",
|
|
5190
|
+
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
/**
|
|
5195
|
+
* Check wallet balance and throw if insufficient
|
|
5196
|
+
*/
|
|
5197
|
+
async _checkBalance() {
|
|
5198
|
+
const balances = await this.walletAdapter.getBalances();
|
|
5199
|
+
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
5200
|
+
if (totalBalance <= 0) {
|
|
5201
|
+
throw new InsufficientBalanceError(1, 0);
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
/**
|
|
5205
|
+
* Spend a token using CashuSpender with standardized error handling
|
|
5206
|
+
*/
|
|
5207
|
+
async _spendToken(params) {
|
|
5208
|
+
const { mintUrl, amount, baseUrl } = params;
|
|
5209
|
+
this._log(
|
|
5210
|
+
"DEBUG",
|
|
5211
|
+
`[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
|
|
4393
5212
|
);
|
|
4394
|
-
if (
|
|
4395
|
-
|
|
5213
|
+
if (this.mode === "apikeys") {
|
|
5214
|
+
let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
5215
|
+
if (!parentApiKey) {
|
|
5216
|
+
this._log(
|
|
5217
|
+
"DEBUG",
|
|
5218
|
+
`[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
|
|
5219
|
+
);
|
|
5220
|
+
const spendResult2 = await this.cashuSpender.spend({
|
|
5221
|
+
mintUrl,
|
|
5222
|
+
amount: amount * TOPUP_MARGIN,
|
|
5223
|
+
baseUrl: "",
|
|
5224
|
+
reuseToken: false
|
|
5225
|
+
});
|
|
5226
|
+
if (!spendResult2.token) {
|
|
5227
|
+
this._log(
|
|
5228
|
+
"ERROR",
|
|
5229
|
+
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
|
|
5230
|
+
spendResult2.error
|
|
5231
|
+
);
|
|
5232
|
+
throw new Error(
|
|
5233
|
+
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
|
|
5234
|
+
);
|
|
5235
|
+
} else {
|
|
5236
|
+
this._log(
|
|
5237
|
+
"DEBUG",
|
|
5238
|
+
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
|
|
5239
|
+
);
|
|
5240
|
+
}
|
|
5241
|
+
this._log(
|
|
5242
|
+
"DEBUG",
|
|
5243
|
+
`[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
|
|
5244
|
+
);
|
|
5245
|
+
try {
|
|
5246
|
+
this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
|
|
5247
|
+
} catch (error) {
|
|
5248
|
+
if (error instanceof Error && error.message.includes("ApiKey already exists")) {
|
|
5249
|
+
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
5250
|
+
spendResult2.token
|
|
5251
|
+
);
|
|
5252
|
+
if (tryReceiveTokenResult.success) {
|
|
5253
|
+
this._log(
|
|
5254
|
+
"DEBUG",
|
|
5255
|
+
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
5256
|
+
);
|
|
5257
|
+
} else {
|
|
5258
|
+
this._log(
|
|
5259
|
+
"DEBUG",
|
|
5260
|
+
`[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
this._log(
|
|
5264
|
+
"DEBUG",
|
|
5265
|
+
`[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
|
|
5266
|
+
);
|
|
5267
|
+
} else {
|
|
5268
|
+
throw error;
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
5272
|
+
} else {
|
|
5273
|
+
this._log(
|
|
5274
|
+
"DEBUG",
|
|
5275
|
+
`[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
|
|
5276
|
+
);
|
|
5277
|
+
}
|
|
5278
|
+
let tokenBalance = 0;
|
|
5279
|
+
let tokenBalanceUnit = "sat";
|
|
5280
|
+
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
5281
|
+
const distributionForBaseUrl = apiKeyDistribution.find(
|
|
5282
|
+
(d) => d.baseUrl === baseUrl
|
|
5283
|
+
);
|
|
5284
|
+
if (distributionForBaseUrl) {
|
|
5285
|
+
tokenBalance = distributionForBaseUrl.amount;
|
|
5286
|
+
}
|
|
5287
|
+
if (tokenBalance === 0 && parentApiKey) {
|
|
5288
|
+
try {
|
|
5289
|
+
const balanceInfo = await this.balanceManager.getTokenBalance(
|
|
5290
|
+
parentApiKey.key,
|
|
5291
|
+
baseUrl
|
|
5292
|
+
);
|
|
5293
|
+
tokenBalance = balanceInfo.amount;
|
|
5294
|
+
tokenBalanceUnit = balanceInfo.unit;
|
|
5295
|
+
} catch (e) {
|
|
5296
|
+
this._log("WARN", "Could not get initial API key balance:", e);
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
this._log(
|
|
5300
|
+
"DEBUG",
|
|
5301
|
+
`[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
|
|
5302
|
+
);
|
|
5303
|
+
return {
|
|
5304
|
+
token: parentApiKey?.key ?? "",
|
|
5305
|
+
tokenBalance,
|
|
5306
|
+
tokenBalanceUnit
|
|
5307
|
+
};
|
|
4396
5308
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
key,
|
|
4401
|
-
balance: 0,
|
|
4402
|
-
lastUsed: Date.now()
|
|
4403
|
-
});
|
|
4404
|
-
store.getState().setApiKeys(next);
|
|
4405
|
-
},
|
|
4406
|
-
updateApiKeyBalance: (baseUrl, balance) => {
|
|
4407
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4408
|
-
const keys = store.getState().apiKeys;
|
|
4409
|
-
const next = keys.map(
|
|
4410
|
-
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
4411
|
-
);
|
|
4412
|
-
store.getState().setApiKeys(next);
|
|
4413
|
-
},
|
|
4414
|
-
removeApiKey: (baseUrl) => {
|
|
4415
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4416
|
-
const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
|
|
4417
|
-
store.getState().setApiKeys(next);
|
|
4418
|
-
},
|
|
4419
|
-
getAllApiKeys: () => {
|
|
4420
|
-
return store.getState().apiKeys.map((entry) => ({
|
|
4421
|
-
baseUrl: entry.baseUrl,
|
|
4422
|
-
key: entry.key,
|
|
4423
|
-
balance: entry.balance,
|
|
4424
|
-
lastUsed: entry.lastUsed
|
|
4425
|
-
}));
|
|
4426
|
-
},
|
|
4427
|
-
// ========== Child Keys ==========
|
|
4428
|
-
getChildKey: (parentBaseUrl) => {
|
|
4429
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4430
|
-
const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
|
|
4431
|
-
if (!entry) return null;
|
|
4432
|
-
return {
|
|
4433
|
-
parentBaseUrl: entry.parentBaseUrl,
|
|
4434
|
-
childKey: entry.childKey,
|
|
4435
|
-
balance: entry.balance,
|
|
4436
|
-
balanceLimit: entry.balanceLimit,
|
|
4437
|
-
validityDate: entry.validityDate,
|
|
4438
|
-
createdAt: entry.createdAt
|
|
4439
|
-
};
|
|
4440
|
-
},
|
|
4441
|
-
setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
|
|
4442
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4443
|
-
const keys = store.getState().childKeys;
|
|
4444
|
-
const existingIndex = keys.findIndex(
|
|
4445
|
-
(entry) => entry.parentBaseUrl === normalized
|
|
5309
|
+
this._log(
|
|
5310
|
+
"DEBUG",
|
|
5311
|
+
`[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
|
|
4446
5312
|
);
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
5313
|
+
const spendResult = await this.cashuSpender.spend({
|
|
5314
|
+
mintUrl,
|
|
5315
|
+
amount,
|
|
5316
|
+
baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
|
|
5317
|
+
reuseToken: this.mode === "lazyrefund"
|
|
5318
|
+
});
|
|
5319
|
+
if (!spendResult.token) {
|
|
5320
|
+
this._log(
|
|
5321
|
+
"ERROR",
|
|
5322
|
+
`[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
|
|
5323
|
+
spendResult.error
|
|
4457
5324
|
);
|
|
4458
|
-
store.getState().setChildKeys(next);
|
|
4459
5325
|
} else {
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
balance: balance ?? 0,
|
|
4465
|
-
validityDate,
|
|
4466
|
-
balanceLimit,
|
|
4467
|
-
createdAt: Date.now()
|
|
4468
|
-
});
|
|
4469
|
-
store.getState().setChildKeys(next);
|
|
4470
|
-
}
|
|
4471
|
-
},
|
|
4472
|
-
updateChildKeyBalance: (parentBaseUrl, balance) => {
|
|
4473
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4474
|
-
const keys = store.getState().childKeys;
|
|
4475
|
-
const next = keys.map(
|
|
4476
|
-
(entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
|
|
4477
|
-
);
|
|
4478
|
-
store.getState().setChildKeys(next);
|
|
4479
|
-
},
|
|
4480
|
-
removeChildKey: (parentBaseUrl) => {
|
|
4481
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4482
|
-
const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
|
|
4483
|
-
store.getState().setChildKeys(next);
|
|
4484
|
-
},
|
|
4485
|
-
getAllChildKeys: () => {
|
|
4486
|
-
return store.getState().childKeys.map((entry) => ({
|
|
4487
|
-
parentBaseUrl: entry.parentBaseUrl,
|
|
4488
|
-
childKey: entry.childKey,
|
|
4489
|
-
balance: entry.balance,
|
|
4490
|
-
balanceLimit: entry.balanceLimit,
|
|
4491
|
-
validityDate: entry.validityDate,
|
|
4492
|
-
createdAt: entry.createdAt
|
|
4493
|
-
}));
|
|
4494
|
-
},
|
|
4495
|
-
getCachedReceiveTokens: () => {
|
|
4496
|
-
return store.getState().cachedReceiveTokens;
|
|
4497
|
-
},
|
|
4498
|
-
setCachedReceiveTokens: (tokens) => {
|
|
4499
|
-
store.getState().setCachedReceiveTokens(tokens);
|
|
4500
|
-
}
|
|
4501
|
-
});
|
|
4502
|
-
var createProviderRegistryFromStore = (store) => ({
|
|
4503
|
-
getModelsForProvider: (baseUrl) => {
|
|
4504
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4505
|
-
return store.getState().modelsFromAllProviders[normalized] || [];
|
|
4506
|
-
},
|
|
4507
|
-
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4508
|
-
getProviderMints: (baseUrl) => {
|
|
4509
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4510
|
-
return store.getState().mintsFromAllProviders[normalized] || [];
|
|
4511
|
-
},
|
|
4512
|
-
getProviderInfo: async (baseUrl) => {
|
|
4513
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4514
|
-
const cached = store.getState().infoFromAllProviders[normalized];
|
|
4515
|
-
if (cached) return cached;
|
|
4516
|
-
try {
|
|
4517
|
-
const response = await fetch(`${normalized}v1/info`);
|
|
4518
|
-
if (!response.ok) {
|
|
4519
|
-
throw new Error(`Failed ${response.status}`);
|
|
4520
|
-
}
|
|
4521
|
-
const info = await response.json();
|
|
4522
|
-
const next = { ...store.getState().infoFromAllProviders };
|
|
4523
|
-
next[normalized] = info;
|
|
4524
|
-
store.getState().setInfoFromAllProviders(next);
|
|
4525
|
-
return info;
|
|
4526
|
-
} catch (error) {
|
|
4527
|
-
console.warn(`Failed to fetch provider info from ${normalized}:`, error);
|
|
4528
|
-
return null;
|
|
5326
|
+
this._log(
|
|
5327
|
+
"DEBUG",
|
|
5328
|
+
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
|
|
5329
|
+
);
|
|
4529
5330
|
}
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
var isBrowser2 = () => {
|
|
4536
|
-
try {
|
|
4537
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
4538
|
-
} catch {
|
|
4539
|
-
return false;
|
|
4540
|
-
}
|
|
4541
|
-
};
|
|
4542
|
-
var isNode = () => {
|
|
4543
|
-
try {
|
|
4544
|
-
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
4545
|
-
} catch {
|
|
4546
|
-
return false;
|
|
4547
|
-
}
|
|
4548
|
-
};
|
|
4549
|
-
var defaultDriver = null;
|
|
4550
|
-
var isBun2 = () => {
|
|
4551
|
-
return typeof process.versions.bun !== "undefined";
|
|
4552
|
-
};
|
|
4553
|
-
var getDefaultSdkDriver = () => {
|
|
4554
|
-
if (defaultDriver) return defaultDriver;
|
|
4555
|
-
if (isBrowser2()) {
|
|
4556
|
-
defaultDriver = localStorageDriver;
|
|
4557
|
-
return defaultDriver;
|
|
4558
|
-
}
|
|
4559
|
-
if (isBun2()) {
|
|
4560
|
-
defaultDriver = createMemoryDriver();
|
|
4561
|
-
return defaultDriver;
|
|
5331
|
+
return {
|
|
5332
|
+
token: spendResult.token,
|
|
5333
|
+
tokenBalance: spendResult.balance,
|
|
5334
|
+
tokenBalanceUnit: spendResult.unit ?? "sat"
|
|
5335
|
+
};
|
|
4562
5336
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
5337
|
+
/**
|
|
5338
|
+
* Build request headers with common defaults and dev mock controls
|
|
5339
|
+
*/
|
|
5340
|
+
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
5341
|
+
const headers = {
|
|
5342
|
+
...additionalHeaders,
|
|
5343
|
+
"Content-Type": "application/json"
|
|
5344
|
+
};
|
|
5345
|
+
return headers;
|
|
4566
5346
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
5347
|
+
/**
|
|
5348
|
+
* Attach auth headers using the active client mode
|
|
5349
|
+
*/
|
|
5350
|
+
_withAuthHeader(headers, token) {
|
|
5351
|
+
const nextHeaders = { ...headers };
|
|
5352
|
+
if (this.mode === "xcashu") {
|
|
5353
|
+
nextHeaders["X-Cashu"] = token;
|
|
5354
|
+
} else {
|
|
5355
|
+
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
5356
|
+
}
|
|
5357
|
+
return nextHeaders;
|
|
4574
5358
|
}
|
|
4575
|
-
return defaultStore.hydrate.then(() => defaultStore.store);
|
|
4576
5359
|
};
|
|
4577
|
-
var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
|
|
4578
|
-
var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
|
|
4579
|
-
var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
|
|
4580
5360
|
|
|
4581
5361
|
// routeRequests.ts
|
|
4582
|
-
async function
|
|
5362
|
+
async function resolveRouteRequestContext(options) {
|
|
4583
5363
|
const {
|
|
4584
5364
|
modelId,
|
|
4585
5365
|
requestBody,
|
|
@@ -4653,12 +5433,11 @@ async function routeRequests(options) {
|
|
|
4653
5433
|
if (!mintUrl) {
|
|
4654
5434
|
throw new Error("No mint configured in wallet");
|
|
4655
5435
|
}
|
|
4656
|
-
const alertLevel = "min";
|
|
4657
5436
|
const client = new RoutstrClient(
|
|
4658
5437
|
walletAdapter,
|
|
4659
5438
|
storageAdapter,
|
|
4660
5439
|
providerRegistry,
|
|
4661
|
-
|
|
5440
|
+
"min",
|
|
4662
5441
|
mode
|
|
4663
5442
|
);
|
|
4664
5443
|
if (debugLevel) {
|
|
@@ -4666,18 +5445,27 @@ async function routeRequests(options) {
|
|
|
4666
5445
|
}
|
|
4667
5446
|
const maxTokens = extractMaxTokens(requestBody);
|
|
4668
5447
|
const stream = extractStream(requestBody);
|
|
4669
|
-
|
|
5448
|
+
const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
|
|
5449
|
+
proxiedBody.model = selectedModel.id;
|
|
5450
|
+
if (stream !== void 0) {
|
|
5451
|
+
proxiedBody.stream = stream;
|
|
5452
|
+
}
|
|
5453
|
+
if (maxTokens !== void 0) {
|
|
5454
|
+
proxiedBody.max_tokens = maxTokens;
|
|
5455
|
+
}
|
|
5456
|
+
return {
|
|
5457
|
+
client,
|
|
5458
|
+
baseUrl,
|
|
5459
|
+
mintUrl,
|
|
5460
|
+
path,
|
|
5461
|
+
modelId,
|
|
5462
|
+
proxiedBody
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
async function routeRequests(options) {
|
|
5466
|
+
const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
|
|
4670
5467
|
try {
|
|
4671
|
-
const
|
|
4672
|
-
proxiedBody.model = selectedModel.id;
|
|
4673
|
-
if (stream !== void 0) {
|
|
4674
|
-
proxiedBody.stream = stream;
|
|
4675
|
-
}
|
|
4676
|
-
if (maxTokens !== void 0) {
|
|
4677
|
-
proxiedBody.max_tokens = maxTokens;
|
|
4678
|
-
}
|
|
4679
|
-
console.log(modelId);
|
|
4680
|
-
response = await client.routeRequest({
|
|
5468
|
+
const response = await client.routeRequest({
|
|
4681
5469
|
path,
|
|
4682
5470
|
method: "POST",
|
|
4683
5471
|
body: proxiedBody,
|
|
@@ -4696,24 +5484,41 @@ async function routeRequests(options) {
|
|
|
4696
5484
|
throw error;
|
|
4697
5485
|
}
|
|
4698
5486
|
}
|
|
5487
|
+
async function routeRequestsToNodeResponse(options) {
|
|
5488
|
+
const { res } = options;
|
|
5489
|
+
const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
|
|
5490
|
+
try {
|
|
5491
|
+
await client.routeRequestToNodeResponse({
|
|
5492
|
+
path,
|
|
5493
|
+
method: "POST",
|
|
5494
|
+
body: proxiedBody,
|
|
5495
|
+
baseUrl,
|
|
5496
|
+
mintUrl,
|
|
5497
|
+
modelId,
|
|
5498
|
+
res
|
|
5499
|
+
});
|
|
5500
|
+
} catch (error) {
|
|
5501
|
+
if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
|
|
5502
|
+
throw new Error(`Authentication failed: ${error.message}`);
|
|
5503
|
+
}
|
|
5504
|
+
throw error;
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
4699
5507
|
function extractMaxTokens(requestBody) {
|
|
4700
5508
|
if (!requestBody || typeof requestBody !== "object") {
|
|
4701
5509
|
return void 0;
|
|
4702
5510
|
}
|
|
4703
5511
|
const body = requestBody;
|
|
4704
5512
|
const maxTokens = body.max_tokens;
|
|
4705
|
-
|
|
4706
|
-
return maxTokens;
|
|
4707
|
-
}
|
|
4708
|
-
return void 0;
|
|
5513
|
+
return typeof maxTokens === "number" ? maxTokens : void 0;
|
|
4709
5514
|
}
|
|
4710
5515
|
function extractStream(requestBody) {
|
|
4711
5516
|
if (!requestBody || typeof requestBody !== "object") {
|
|
4712
|
-
return
|
|
5517
|
+
return void 0;
|
|
4713
5518
|
}
|
|
4714
5519
|
const body = requestBody;
|
|
4715
5520
|
const stream = body.stream;
|
|
4716
|
-
return stream ===
|
|
5521
|
+
return typeof stream === "boolean" ? stream : void 0;
|
|
4717
5522
|
}
|
|
4718
5523
|
|
|
4719
5524
|
exports.BalanceManager = BalanceManager;
|
|
@@ -4736,10 +5541,14 @@ exports.StreamingError = StreamingError;
|
|
|
4736
5541
|
exports.TokenOperationError = TokenOperationError;
|
|
4737
5542
|
exports.createDiscoveryAdapterFromStore = createDiscoveryAdapterFromStore;
|
|
4738
5543
|
exports.createIndexedDBDriver = createIndexedDBDriver;
|
|
5544
|
+
exports.createIndexedDBUsageTrackingDriver = createIndexedDBUsageTrackingDriver;
|
|
4739
5545
|
exports.createMemoryDriver = createMemoryDriver;
|
|
5546
|
+
exports.createMemoryUsageTrackingDriver = createMemoryUsageTrackingDriver;
|
|
4740
5547
|
exports.createProviderRegistryFromStore = createProviderRegistryFromStore;
|
|
5548
|
+
exports.createSSEParserTransform = createSSEParserTransform;
|
|
4741
5549
|
exports.createSdkStore = createSdkStore;
|
|
4742
5550
|
exports.createSqliteDriver = createSqliteDriver;
|
|
5551
|
+
exports.createSqliteUsageTrackingDriver = createSqliteUsageTrackingDriver;
|
|
4743
5552
|
exports.createStorageAdapterFromStore = createStorageAdapterFromStore;
|
|
4744
5553
|
exports.filterBaseUrlsForTor = filterBaseUrlsForTor;
|
|
4745
5554
|
exports.getDefaultDiscoveryAdapter = getDefaultDiscoveryAdapter;
|
|
@@ -4747,11 +5556,13 @@ exports.getDefaultProviderRegistry = getDefaultProviderRegistry;
|
|
|
4747
5556
|
exports.getDefaultSdkDriver = getDefaultSdkDriver;
|
|
4748
5557
|
exports.getDefaultSdkStore = getDefaultSdkStore;
|
|
4749
5558
|
exports.getDefaultStorageAdapter = getDefaultStorageAdapter;
|
|
5559
|
+
exports.getDefaultUsageTrackingDriver = getDefaultUsageTrackingDriver;
|
|
4750
5560
|
exports.getProviderEndpoints = getProviderEndpoints;
|
|
4751
5561
|
exports.isOnionUrl = isOnionUrl;
|
|
4752
5562
|
exports.isTorContext = isTorContext;
|
|
4753
5563
|
exports.localStorageDriver = localStorageDriver;
|
|
4754
5564
|
exports.normalizeProviderUrl = normalizeProviderUrl;
|
|
4755
5565
|
exports.routeRequests = routeRequests;
|
|
5566
|
+
exports.routeRequestsToNodeResponse = routeRequestsToNodeResponse;
|
|
4756
5567
|
//# sourceMappingURL=index.js.map
|
|
4757
5568
|
//# sourceMappingURL=index.js.map
|